# ============================================================================
# [LIVRE] PYTHON POO - GUIDE ULTRA-DÉTAILLÉ POUR DÉBUTANTS
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR MAÎTRISER LA POO EN PYTHON DE ZÉRO À EXPERT
#
# Ce guide est organisé en 4 parties progressives :
#
# PARTIE 1 : FONDAMENTAUX DE LA POO (poo_partie1.txt)
# - Chapitre 0 : Introduction à la POO
# - Chapitre 1 : Premières Classes
# - Chapitre 2 : Attributs et Méthodes
# - Chapitre 3 : Le Constructeur __init__
# - Chapitre 4 : Méthodes Spéciales
#
# PARTIE 2 : HÉRITAGE ET POLYMORPHISME (poo_partie2.txt)
# - Chapitre 5 : Héritage Simple
# - Chapitre 6 : Héritage Multiple
# - Chapitre 7 : Polymorphisme
# - Chapitre 8 : Classes Abstraites
#
# PARTIE 3 : CONCEPTS AVANCÉS (poo_partie3.txt)
# - Chapitre 9 : Encapsulation et Propriétés
# - Chapitre 10 : Méthodes de Classe et Statiques
# - Chapitre 11 : Décorateurs et Métaclasses
# - Chapitre 12 : Dataclasses
# - Chapitre 13 : Composition vs Héritage
#
# PARTIE 4 : PATTERNS ET BEST PRACTICES (poo_partie4.txt)
# - Chapitre 14 : Design Patterns Essentiels
# - Chapitre 15 : SOLID Principles
# - Chapitre 16 : Testing de Classes
# - Chapitre 17 : Documentation
# - Chapitre 18 : Performance et Optimisation
# - Chapitre 19 : Erreurs Courantes
# - Chapitre 20 : Best Practices
#
# [TEMPS] TEMPS DE LECTURE TOTAL : ~20-25 heures
# [DOCS] PRÉREQUIS : Python de base (variables, fonctions, boucles)
#
# [IDEE] COMMENT UTILISER CE GUIDE :
# 1. Lisez les parties dans l'ordre
# 2. Testez TOUS les exemples dans Python
# 3. Faites les exercices pratiques
# 4. Créez vos propres classes
#
# ============================================================================

"""
[OBJECTIF] PHILOSOPHIE DE CE GUIDE

COMMENT ? -> Explications pas à pas avec code
POURQUOI ? -> Raisons et contexte réel
QUAND ? -> Cas d'usage concrets
PRATIQUE -> Exemples réels et exercices

Ce guide vise à être VOTRE SEULE RÉFÉRENCE POO Python !
"""

# ============================================================================
# [NOTE] CONVENTIONS UTILISÉES DANS CE GUIDE
# ============================================================================

"""
[IDEE] Information importante
[REFLEXION] Question / Réflexion
[OK] Bonne pratique
[X] Mauvaise pratique
[ATTENTION] Attention / Avertissement
[CLE] Point clé à retenir
[COURS] Exercice pratique
[DOCS] Résumé
[OBJECTIF] Objectif
[TEMPS] Temps estimé
[RAPIDE] Prêt pour la suite
"""

# ============================================================================
# [OUTILS] CONFIGURATION DE L'ENVIRONNEMENT
# ============================================================================

"""
AVANT DE COMMENCER

1. VÉRIFIER PYTHON
   - Version recommandée : Python 3.8+
   - Vérifier : python --version ou python3 --version

2. ÉDITEUR DE CODE
   - VS Code (recommandé) : https://code.visualstudio.com/
   - PyCharm Community : https://www.jetbrains.com/pycharm/
   - Jupyter Notebook : Pour expérimentation interactive

3. CRÉER UN DOSSIER DE TRAVAIL
"""

# Structure recommandée
"""
apprendre_poo/
├── exemples/
│   ├── chapitre1.py
│   ├── chapitre2.py
│   └── ...
├── exercices/
│   ├── exercice1.py
│   ├── exercice2.py
│   └── ...
└── projets/
    ├── projet1/
    └── projet2/
"""

"""
4. TESTER PYTHON INTERACTIF
"""

# Ouvrir terminal/cmd et taper :
python
# ou
python3

# Dans l'interpréteur Python :
>>> print("Hello, POO!")
Hello, POO!
>>> exit()

"""
[IDEE] POURQUOI UN ENVIRONNEMENT PROPRE ?

[OK] Organisation claire
[OK] Facile de retrouver exemples
[OK] Séparation exemples/exercices/projets
[OK] Historique de progression
"""


# ============================================================================
# [GUIDE] CHAPITRE 0 : INTRODUCTION À LA POO
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est la Programmation Orientée Objet
[OK] Pourquoi utiliser la POO
[OK] Les concepts fondamentaux (Classes, Objets, etc.)
[OK] Différence POO vs Programmation Procédurale
[OK] Quand utiliser la POO
"""


# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QUE LA POO ?
# ----------------------------------------------------------------------------

"""
DÉFINITION SIMPLE

POO = Programmation Orientée Objet

C'est une façon de programmer qui organise le code autour d'OBJETS
plutôt que de fonctions et de logique.

[IDEE] OBJET = Entité qui contient :
- Des DONNÉES (attributs)
- Des COMPORTEMENTS (méthodes)


ANALOGIE DU MONDE RÉEL [VOITURE]

Pensez à une voiture :

DONNÉES (Ce qu'elle a) :
- Marque : "Toyota"
- Modèle : "Corolla"
- Couleur : "Rouge"
- Vitesse actuelle : 0 km/h
- Carburant : 50 litres

COMPORTEMENTS (Ce qu'elle peut faire) :
- Démarrer()
- Accélérer()
- Freiner()
- Klaxonner()
- Faire le plein()


En POO, on créerait une "classe" Voiture qui définit :
- Quelles données une voiture possède
- Quelles actions une voiture peut faire

Puis on créerait des "objets" voitures spécifiques :
- ma_voiture = Voiture("Toyota", "Corolla", "Rouge")
- ta_voiture = Voiture("Honda", "Civic", "Bleue")
"""


# ----------------------------------------------------------------------------
# [RECHERCHE] PROGRAMMATION PROCÉDURALE VS POO
# ----------------------------------------------------------------------------

"""
COMPARAISON AVEC UN EXEMPLE CONCRET


PROBLÈME : Gérer des comptes bancaires


[X] APPROCHE PROCÉDURALE (Sans POO)
"""

# Variables globales (données éparpillées)
compte1_numero = "123456"
compte1_titulaire = "Alice"
compte1_solde = 1000.0

compte2_numero = "789012"
compte2_titulaire = "Bob"
compte2_solde = 500.0

# Fonctions (comportements séparés des données)
def deposer(compte_numero, titulaire, solde, montant):
    """Déposer de l'argent"""
    if montant > 0:
        solde += montant
        print(f"Dépôt de {montant}€. Nouveau solde : {solde}€")
        return solde
    else:
        print("Montant invalide")
        return solde

def retirer(compte_numero, titulaire, solde, montant):
    """Retirer de l'argent"""
    if montant > 0 and solde >= montant:
        solde -= montant
        print(f"Retrait de {montant}€. Nouveau solde : {solde}€")
        return solde
    else:
        print("Retrait impossible")
        return solde

# Utilisation (compliqué et répétitif)
compte1_solde = deposer(compte1_numero, compte1_titulaire, compte1_solde, 200)
compte1_solde = retirer(compte1_numero, compte1_titulaire, compte1_solde, 50)

compte2_solde = deposer(compte2_numero, compte2_titulaire, compte2_solde, 100)

"""
[X] PROBLÈMES DE L'APPROCHE PROCÉDURALE :

1. DONNÉES ÉPARPILLÉES
   - compte1_numero, compte1_titulaire, compte1_solde séparés
   - Facile d'oublier une variable
   - Pas de lien évident entre elles

2. RÉPÉTITION
   - Passer 3-4 paramètres à chaque fonction
   - Code verbeux et répétitif

3. PAS DE PROTECTION
   - Rien n'empêche : compte1_solde = -1000 (invalide!)
   - Données modifiables n'importe où

4. DIFFICILE À MAINTENIR
   - Ajouter champ = modifier toutes les fonctions
   - Risque d'oublis et d'erreurs

5. PAS DE RÉUTILISATION
   - Difficile de créer un nouveau compte
   - Beaucoup de code dupliqué


[OK] APPROCHE POO (Avec Classes et Objets)
"""

class CompteBancaire:
    """
    [IDEE] CLASSE = Modèle/Plan pour créer des comptes
    
    Définit :
    - Quelles données un compte possède
    - Quelles actions un compte peut faire
    """
    
    def __init__(self, numero, titulaire, solde_initial=0):
        """
        Constructeur : Initialise un nouveau compte
        """
        self.numero = numero
        self.titulaire = titulaire
        self.solde = solde_initial
    
    def deposer(self, montant):
        """Déposer de l'argent"""
        if montant > 0:
            self.solde += montant
            print(f"Dépôt de {montant}€. Nouveau solde : {self.solde}€")
        else:
            print("Montant invalide")
    
    def retirer(self, montant):
        """Retirer de l'argent"""
        if montant > 0 and self.solde >= montant:
            self.solde -= montant
            print(f"Retrait de {montant}€. Nouveau solde : {self.solde}€")
        else:
            print("Retrait impossible")
    
    def afficher_solde(self):
        """Afficher le solde"""
        print(f"Compte {self.numero} ({self.titulaire}) : {self.solde}€")

# Utilisation (simple et claire !)
compte_alice = CompteBancaire("123456", "Alice", 1000)
compte_bob = CompteBancaire("789012", "Bob", 500)

# Actions simples et intuitives
compte_alice.deposer(200)    # Dépôt de 200€. Nouveau solde : 1200€
compte_alice.retirer(50)     # Retrait de 50€. Nouveau solde : 1150€

compte_bob.deposer(100)      # Dépôt de 100€. Nouveau solde : 600€

compte_alice.afficher_solde()  # Compte 123456 (Alice) : 1150€
compte_bob.afficher_solde()    # Compte 789012 (Bob) : 600€

"""
[OK] AVANTAGES DE L'APPROCHE POO :

1. DONNÉES REGROUPÉES
   - Toutes les infos d'un compte ensemble
   - numero, titulaire, solde liés naturellement
   - Structure claire et logique

2. CODE PLUS COURT ET LISIBLE
   - compte.deposer(200) vs deposer(numero, titulaire, solde, 200)
   - Intention claire
   - Moins de paramètres

3. PROTECTION DES DONNÉES
   - Méthodes contrôlent l'accès
   - Validation intégrée
   - Impossible de mettre solde négatif par erreur

4. FACILE À MAINTENIR
   - Ajouter champ = modifier seulement la classe
   - Changements localisés
   - Moins de risques d'erreurs

5. RÉUTILISATION
   - Créer nouveau compte = 1 ligne
   - Même comportement garanti
   - Code DRY (Don't Repeat Yourself)

6. EXTENSIBILITÉ
   - Facile d'ajouter CompteEpargne, CompteCourant
   - Héritage (on verra plus tard)
"""


# ----------------------------------------------------------------------------
# [MODULE] CONCEPTS FONDAMENTAUX DE LA POO
# ----------------------------------------------------------------------------

"""
LES 4 PILIERS DE LA POO


1⃣ ENCAPSULATION
─────────────────

[IDEE] Regrouper données + comportements dans une seule entité

Exemple :
"""

class Voiture:
    def __init__(self, marque, modele):
        self.marque = marque      # Données
        self.modele = modele      # Données
        self.vitesse = 0          # Données
    
    def accelerer(self):          # Comportement
        self.vitesse += 10
    
    def freiner(self):            # Comportement
        self.vitesse -= 10

"""
[OK] Tout ce qui concerne une voiture est DANS la classe Voiture
[OK] Données (marque, modele, vitesse) protégées
[OK] Modifications contrôlées par méthodes (accelerer, freiner)


2⃣ HÉRITAGE
────────────

[IDEE] Créer nouvelle classe basée sur classe existante

Exemple :
"""

class Animal:
    def __init__(self, nom):
        self.nom = nom
    
    def manger(self):
        print(f"{self.nom} mange")

class Chien(Animal):  # Chien hérite d'Animal
    def aboyer(self):
        print(f"{self.nom} aboie : Wouf!")

class Chat(Animal):   # Chat hérite d'Animal
    def miauler(self):
        print(f"{self.nom} miaule : Miaou!")

# Utilisation
rex = Chien("Rex")
rex.manger()   # Hérité d'Animal
rex.aboyer()   # Spécifique à Chien

felix = Chat("Felix")
felix.manger()   # Hérité d'Animal
felix.miauler()  # Spécifique à Chat

"""
[OK] Chien et Chat RÉUTILISENT le code d'Animal
[OK] Pas de duplication
[OK] Hiérarchie logique


3⃣ POLYMORPHISME
─────────────────

[IDEE] Même interface, comportements différents

Exemple :
"""

class Animal:
    def faire_son(self):
        pass

class Chien(Animal):
    def faire_son(self):
        return "Wouf!"

class Chat(Animal):
    def faire_son(self):
        return "Miaou!"

class Vache(Animal):
    def faire_son(self):
        return "Meuh!"

# Même méthode, résultats différents !
animaux = [Chien(), Chat(), Vache()]

for animal in animaux:
    print(animal.faire_son())
# Wouf!
# Miaou!
# Meuh!

"""
[OK] Même méthode faire_son()
[OK] Comportement différent selon l'objet
[OK] Code flexible et extensible


4⃣ ABSTRACTION
───────────────

[IDEE] Cacher la complexité, montrer seulement l'essentiel

Exemple :
"""

class Voiture:
    def demarrer(self):
        """L'utilisateur voit juste "démarrer" """
        self._verifier_carburant()      # Complexité cachée
        self._allumer_moteur()          # Complexité cachée
        self._initialiser_systemes()    # Complexité cachée
        print("Voiture démarrée !")
    
    def _verifier_carburant(self):
        # Détails internes cachés
        pass
    
    def _allumer_moteur(self):
        # Détails internes cachés
        pass
    
    def _initialiser_systemes(self):
        # Détails internes cachés
        pass

# Utilisation simple
ma_voiture = Voiture()
ma_voiture.demarrer()  # Simple à utiliser !

"""
[OK] Utilisateur ne voit que demarrer()
[OK] Complexité interne cachée
[OK] Interface simple
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] QUAND UTILISER LA POO ?
# ----------------------------------------------------------------------------

"""
CAS D'USAGE IDÉAUX POUR LA POO


[OK] MODÉLISER DES ENTITÉS DU MONDE RÉEL
────────────────────────────────────────

Quand : Vous avez des "choses" avec propriétés et actions

Exemples :
- Application de gestion : Utilisateur, Produit, Commande
- Jeu vidéo : Joueur, Ennemi, Arme, Niveau
- Système bancaire : Compte, Transaction, Client
- Réseau social : Post, Commentaire, Utilisateur

Code :
"""

class Utilisateur:
    def __init__(self, nom, email):
        self.nom = nom
        self.email = email
        self.posts = []
    
    def creer_post(self, contenu):
        post = Post(contenu, self)
        self.posts.append(post)
        return post

class Post:
    def __init__(self, contenu, auteur):
        self.contenu = contenu
        self.auteur = auteur
        self.likes = 0
    
    def liker(self):
        self.likes += 1

"""
[OK] DONNÉES COMPLEXES AVEC COMPORTEMENTS
────────────────────────────────────────

Quand : Pas juste stocker données, mais aussi les manipuler

Exemples :
- Liste de tâches avec priorités, tags, échéances
- Panier d'achat avec calculs, promotions, taxes
- Configuration avec validation et sauvegarde

Code :
"""

class PanierAchat:
    def __init__(self):
        self.articles = []
        self.code_promo = None
    
    def ajouter_article(self, produit, quantite):
        self.articles.append({'produit': produit, 'quantite': quantite})
    
    def appliquer_promo(self, code):
        self.code_promo = code
    
    def calculer_total(self):
        total = sum(article['produit'].prix * article['quantite'] 
                   for article in self.articles)
        
        if self.code_promo:
            total *= 0.9  # 10% de réduction
        
        return total

"""
[OK] CODE RÉUTILISABLE ET EXTENSIBLE
───────────────────────────────────

Quand : Plusieurs variantes d'une même chose

Exemples :
- Différents types de comptes : Courant, Épargne, Entreprise
- Différents employés : Manager, Développeur, Designer
- Différents véhicules : Voiture, Moto, Camion

Code :
"""

class Employe:
    def __init__(self, nom, salaire_base):
        self.nom = nom
        self.salaire_base = salaire_base
    
    def calculer_salaire(self):
        return self.salaire_base

class Manager(Employe):
    def __init__(self, nom, salaire_base, bonus):
        super().__init__(nom, salaire_base)
        self.bonus = bonus
    
    def calculer_salaire(self):
        return self.salaire_base + self.bonus

class Developpeur(Employe):
    def __init__(self, nom, salaire_base, heures_sup):
        super().__init__(nom, salaire_base)
        self.heures_sup = heures_sup
    
    def calculer_salaire(self):
        return self.salaire_base + (self.heures_sup * 50)

"""
[X] QUAND NE PAS UTILISER LA POO
────────────────────────────────

1. SCRIPTS SIMPLES ET COURTS
   Script de 20 lignes pour renommer fichiers
   -> Pas besoin de classes
   
2. CALCULS MATHÉMATIQUES SIMPLES
   Calculer moyenne de nombres
   -> Fonctions simples suffisent
   
3. TRANSFORMATION DE DONNÉES SIMPLE
   Lire CSV, filtrer, écrire résultat
   -> Pipeline de fonctions plus clair
   
4. PROTOTYPES RAPIDES
   Tester une idée en 5 minutes
   -> Garder simple

Exemple (OK sans POO) :
"""

# Simple et clair pour cette tâche
def calculer_moyenne(nombres):
    return sum(nombres) / len(nombres)

def filtrer_pairs(nombres):
    return [n for n in nombres if n % 2 == 0]

# Utilisation
data = [1, 2, 3, 4, 5, 6]
pairs = filtrer_pairs(data)
moyenne = calculer_moyenne(pairs)
print(moyenne)  # 4.0

"""
[CLE] RÈGLE GÉNÉRALE

Utilisez la POO quand :
[OK] Vous modélisez des entités du monde réel
[OK] Données + comportements vont ensemble
[OK] Code va grandir et évoluer
[OK] Réutilisation importante

N'utilisez PAS la POO quand :
[X] Script simple et court
[X] Pas de réutilisation prévue
[X] Juste transformation de données
[X] Complexité non justifiée
"""


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 0
# ----------------------------------------------------------------------------

"""
CE QUE VOUS AVEZ APPRIS

[OK] POO = Organiser code autour d'objets
[OK] Objet = Données + Comportements
[OK] POO vs Procédural : Code plus maintenable
[OK] 4 Piliers : Encapsulation, Héritage, Polymorphisme, Abstraction
[OK] Quand utiliser (ou non) la POO


[CLE] POINTS CLÉS

1. POO regroupe données et comportements
2. Classe = Modèle/Plan
3. Objet = Instance d'une classe
4. POO rend code plus maintenable et réutilisable
5. Utiliser POO pour modéliser monde réel


[OBJECTIF] AVANT DE CONTINUER

Assurez-vous de comprendre :
[OK] Différence entre classe et objet
[OK] Pourquoi POO est utile
[OK] Quand utiliser POO
[OK] Les 4 piliers (en gros)


-> PROCHAINE ÉTAPE : Chapitre 1 - Premières Classes !

Vous allez apprendre :
- Créer votre première classe
- Instancier des objets
- Ajouter attributs et méthodes
- Le mot-clé self

Prêt ? C'est parti ! [RAPIDE]
"""


# ============================================================================
# [GUIDE] CHAPITRE 1 : PREMIÈRES CLASSES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer une classe simple
[OK] Instancier des objets
[OK] Comprendre le mot-clé self
[OK] Différence entre classe et instance
[OK] Ajouter attributs et méthodes
"""


# ----------------------------------------------------------------------------
# [DEMARRAGE] PREMIÈRE CLASSE : HELLO WORLD
# ----------------------------------------------------------------------------

"""
CLASSE LA PLUS SIMPLE POSSIBLE
"""

class Chien:
    """Une classe représentant un chien"""
    pass  # pass = "ne rien faire pour l'instant"

"""
[IDEE] DÉCRYPTAGE

class Chien:
  │     │
  │     └─── Nom de la classe (PascalCase : première lettre majuscule)
  └─────────── Mot-clé pour déclarer une classe

:
    Début du bloc (comme if, for, def)

\"\"\"...\"\"\
    Docstring : description de la classe (optionnel mais recommandé)

pass
    Instruction vide (classe vide pour l'instant)


CONVENTIONS DE NOMMAGE

[OK] Bonnes pratiques :
class Chien          # PascalCase (recommandé)
class CompteBancaire
class UtilisateurPremium

[X] Mauvaises pratiques :
class chien          # Minuscule (éviter)
class Compte_bancaire  # Snake_case (éviter pour classes)
class utilisateur_premium


CRÉER UN OBJET (INSTANCIATION)
"""

# Créer un objet de type Chien
mon_chien = Chien()
print(mon_chien)  # <__main__.Chien object at 0x...>
print(type(mon_chien))  # <class '__main__.Chien'>

# Créer plusieurs objets
chien1 = Chien()
chien2 = Chien()
chien3 = Chien()

# Chaque objet est unique
print(chien1 is chien2)  # False (objets différents)

"""
[IDEE] CLASSE VS INSTANCE

CLASSE = Moule à gâteau
    ├─ Définit la forme
    ├─ Existe une seule fois
    └─ Ne peut pas être mangé

INSTANCE (OBJET) = Gâteau fait avec le moule
    ├─ Créé à partir du moule
    ├─ Peut en créer plusieurs
    └─ Chacun indépendant


Analogie :
class Chien         -> La race "Chien" (concept)
mon_chien = Chien() -> Rex (chien réel spécifique)
ton_chien = Chien() -> Max (autre chien réel)
"""


# ----------------------------------------------------------------------------
# [PACKAGE] AJOUTER DES ATTRIBUTS
# ----------------------------------------------------------------------------

"""
CLASSE AVEC ATTRIBUTS DE CLASSE
"""

class Chien:
    """Classe représentant un chien"""
    
    # Attributs de classe (partagés par tous les objets)
    espece = "Canis lupus familiaris"
    nb_pattes = 4

# Créer objets
rex = Chien()
max = Chien()

# Accéder aux attributs
print(rex.espece)     # Canis lupus familiaris
print(max.nb_pattes)  # 4

# Attribut partagé !
print(rex.espece is max.espece)  # True (même valeur)

"""
[IDEE] ATTRIBUTS DE CLASSE

- Définis directement dans la classe
- PARTAGÉS par toutes les instances
- Même valeur pour tous les objets
- Utiles pour constantes


ATTRIBUTS D'INSTANCE (Spécifiques à chaque objet)
"""

class Chien:
    def __init__(self, nom, age):
        # Attributs d'instance (différents pour chaque objet)
        self.nom = nom
        self.age = age

# Créer objets avec attributs différents
rex = Chien("Rex", 5)
max = Chien("Max", 3)

# Chaque objet a ses propres valeurs
print(rex.nom)  # Rex
print(rex.age)  # 5

print(max.nom)  # Max
print(max.age)  # 3

"""
[IDEE] __init__ : LE CONSTRUCTEUR

__init__ = Méthode spéciale appelée automatiquement lors de la création

def __init__(self, nom, age):
     │        │     │    │
     │        │     │    └─── Paramètre 2 (age)
     │        │     └────────── Paramètre 1 (nom)
     │        └──────────────── Référence à l'objet lui-même
     └───────────────────────── Nom spécial (double underscore)

self.nom = nom
 │    │     │
 │    │     └─── Valeur passée en paramètre
 │    └─────────── Attribut de l'objet
 └──────────────── L'objet lui-même


POURQUOI self ?

self = "moi-même" en anglais
C'est la référence à l'objet en cours de création/modification
"""

class Chien:
    def __init__(self, nom):
        self.nom = nom
        print(f"Création d'un chien nommé {self.nom}")
        print(f"self = {self}")

rex = Chien("Rex")
# Création d'un chien nommé Rex
# self = <__main__.Chien object at 0x...>

"""
[REFLEXION] POURQUOI self EST NÉCESSAIRE ?

En Python, contrairement à d'autres langages, vous devez EXPLICITEMENT
passer la référence à l'objet.

Quand vous faites :
rex.nom

Python transforme en :
Chien.nom(rex)  <- rex est passé comme premier paramètre !

C'est pourquoi self est toujours premier paramètre.
"""


# ----------------------------------------------------------------------------
# [SCENARIO] AJOUTER DES MÉTHODES
# ----------------------------------------------------------------------------

"""
MÉTHODES = Fonctions à l'intérieur d'une classe
"""

class Chien:
    """Classe Chien avec méthodes"""
    
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
    
    def aboyer(self):
        """Le chien aboie"""
        print(f"{self.nom} fait : Wouf wouf!")
    
    def se_presenter(self):
        """Le chien se présente"""
        print(f"Je m'appelle {self.nom} et j'ai {self.age} ans")
    
    def vieillir(self):
        """Le chien vieillit d'un an"""
        self.age += 1
        print(f"{self.nom} a maintenant {self.age} ans")

# Utilisation
rex = Chien("Rex", 5)

rex.aboyer()           # Rex fait : Wouf wouf!
rex.se_presenter()     # Je m'appelle Rex et j'ai 5 ans
rex.vieillir()         # Rex a maintenant 6 ans
rex.se_presenter()     # Je m'appelle Rex et j'ai 6 ans

"""
[IDEE] ANATOMIE D'UNE MÉTHODE

def aboyer(self):
    │    │     │
    │    │     └─── self obligatoire (référence à l'objet)
    │    └─────────── Nom de la méthode (snake_case)
    └──────────────── Mot-clé def (comme fonction)

    print(f"{self.nom} fait : Wouf wouf!")
           │     │
           │     └─── Accès à l'attribut nom de l'objet
           └─────────── self = l'objet lui-même


MÉTHODE AVEC PARAMÈTRES
"""

class Chien:
    def __init__(self, nom):
        self.nom = nom
        self.satiete = 100  # 0 = affamé, 100 = rassasié
    
    def manger(self, quantite):
        """Le chien mange une certaine quantité"""
        self.satiete += quantite
        if self.satiete > 100:
            self.satiete = 100  # Maximum 100
        print(f"{self.nom} a mangé. Satiété : {self.satiete}")
    
    def jouer(self, duree):
        """Le chien joue pendant un certain temps"""
        self.satiete -= duree * 5
        if self.satiete < 0:
            self.satiete = 0
        print(f"{self.nom} a joué pendant {duree} min. Satiété : {self.satiete}")

# Utilisation
rex = Chien("Rex")

rex.manger(20)      # Rex a mangé. Satiété : 100
rex.jouer(10)       # Rex a joué pendant 10 min. Satiété : 50
rex.manger(30)      # Rex a mangé. Satiété : 80

"""
[IDEE] self EN DÉTAIL

DANS __init__ :
"""
class Chien:
    def __init__(self, nom):
        self.nom = nom  # Créer attribut nom pour cet objet
        print(f"self dans __init__ : {id(self)}")

rex = Chien("Rex")
print(f"rex : {id(rex)}")
# Les IDs sont identiques ! self = rex

"""
DANS UNE MÉTHODE :
"""
class Chien:
    def __init__(self, nom):
        self.nom = nom
    
    def aboyer(self):
        # self = l'objet qui appelle la méthode
        print(f"{self.nom} aboie")
        print(f"self dans aboyer : {id(self)}")

rex = Chien("Rex")
print(f"rex : {id(rex)}")
rex.aboyer()
# Tous les IDs identiques !

"""
QUAND VOUS APPELEZ :
rex.aboyer()

Python fait automatiquement :
Chien.aboyer(rex)  <- rex est passé comme self !


EXEMPLE CONCRET :
"""

class Personne:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
    
    def dire_bonjour(self):
        # self permet d'accéder aux attributs de CETTE personne
        print(f"Bonjour, je m'appelle {self.nom} et j'ai {self.age} ans")
    
    def avoir_anniversaire(self):
        # self permet de modifier CETTE personne
        self.age += 1
        print(f"{self.nom} a maintenant {self.age} ans !")

# Deux personnes différentes
alice = Personne("Alice", 25)
bob = Personne("Bob", 30)

# Chaque méthode agit sur SON objet
alice.dire_bonjour()  # self = alice
# Bonjour, je m'appelle Alice et j'ai 25 ans

bob.dire_bonjour()    # self = bob
# Bonjour, je m'appelle Bob et j'ai 30 ans

alice.avoir_anniversaire()  # Modifie alice
# Alice a maintenant 26 ans !

bob.avoir_anniversaire()    # Modifie bob
# Bob a maintenant 31 ans !

"""
[CLE] POINTS CLÉS SUR self

1. TOUJOURS premier paramètre des méthodes
2. Référence à l'instance actuelle
3. Permet d'accéder aux attributs : self.nom
4. Permet d'appeler d'autres méthodes : self.autre_methode()
5. Python le passe automatiquement, vous n'écrivez pas :
   rex.aboyer()      # [OK] Correct
   rex.aboyer(rex)   # [X] Faux (self passé automatiquement)
"""


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 1 : CLASSE ÉTUDIANT
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer une classe Etudiant

SPÉCIFICATIONS :

1. Attributs :
   - nom : string
   - age : int
   - notes : liste de notes (vide au départ)

2. Méthodes :
   - ajouter_note(note) : Ajoute une note
   - calculer_moyenne() : Retourne la moyenne des notes
   - afficher_info() : Affiche nom, age et moyenne


SOLUTION :
"""

class Etudiant:
    """Classe représentant un étudiant"""
    
    def __init__(self, nom, age):
        """
        Initialise un étudiant
        
        Args:
            nom (str): Nom de l'étudiant
            age (int): Âge de l'étudiant
        """
        self.nom = nom
        self.age = age
        self.notes = []  # Liste vide au départ
    
    def ajouter_note(self, note):
        """
        Ajoute une note à l'étudiant
        
        Args:
            note (float): Note à ajouter (0-20)
        """
        if 0 <= note <= 20:
            self.notes.append(note)
            print(f"Note {note} ajoutée pour {self.nom}")
        else:
            print("Note invalide ! (doit être entre 0 et 20)")
    
    def calculer_moyenne(self):
        """
        Calcule et retourne la moyenne des notes
        
        Returns:
            float: Moyenne des notes, ou 0 si pas de notes
        """
        if len(self.notes) == 0:
            return 0
        
        return sum(self.notes) / len(self.notes)
    
    def afficher_info(self):
        """Affiche les informations de l'étudiant"""
        moyenne = self.calculer_moyenne()
        print(f"\n--- Informations Étudiant ---")
        print(f"Nom    : {self.nom}")
        print(f"Âge    : {self.age} ans")
        print(f"Notes  : {self.notes}")
        print(f"Moyenne: {moyenne:.2f}/20")
        print(f"-----------------------------\n")

# Test de la classe
etudiant1 = Etudiant("Alice", 20)

etudiant1.ajouter_note(15)   # Note 15 ajoutée pour Alice
etudiant1.ajouter_note(12)   # Note 12 ajoutée pour Alice
etudiant1.ajouter_note(18)   # Note 18 ajoutée pour Alice
etudiant1.ajouter_note(25)   # Note invalide ! (doit être entre 0 et 20)

etudiant1.afficher_info()
"""
--- Informations Étudiant ---
Nom    : Alice
Âge    : 20 ans
Notes  : [15, 12, 18]
Moyenne: 15.00/20
-----------------------------
"""

# Créer un deuxième étudiant
etudiant2 = Etudiant("Bob", 22)
etudiant2.ajouter_note(10)
etudiant2.ajouter_note(14)
etudiant2.afficher_info()
"""
--- Informations Étudiant ---
Nom    : Bob
Âge    : 22 ans
Notes  : [10, 14]
Moyenne: 12.00/20
-----------------------------
"""


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 1
# ----------------------------------------------------------------------------

"""
CE QUE VOUS AVEZ APPRIS

[OK] Créer une classe avec class NomClasse:
[OK] Instancier des objets : obj = NomClasse()
[OK] Comprendre self (référence à l'instance)
[OK] Ajouter attributs d'instance dans __init__
[OK] Créer méthodes avec def methode(self):
[OK] Accéder aux attributs avec self.attribut
[OK] Différence classe (moule) vs instance (objet)


[CLE] POINTS CLÉS

1. Classe = Modèle, Instance = Objet créé
2. __init__ = Constructeur (initialisation)
3. self = Référence à l'instance actuelle
4. Attributs = Données de l'objet
5. Méthodes = Comportements de l'objet


[OBJECTIF] AVANT DE CONTINUER

Assurez-vous de pouvoir :
[OK] Créer une classe simple
[OK] Instancier plusieurs objets
[OK] Ajouter __init__ avec paramètres
[OK] Créer méthodes utilisant self
[OK] Comprendre pourquoi self est nécessaire


-> PROCHAINE ÉTAPE : Chapitre 2 - Attributs et Méthodes en Profondeur !

Vous allez apprendre :
- Différents types d'attributs
- Différents types de méthodes
- Attributs privés et protégés
- Getters et setters
- Méthodes de classe et statiques

Continuez ! [RAPIDE]
"""


# ============================================================================
# FIN DE LA PARTIE 1 - FICHIER 1
# ============================================================================

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 1 (SUITE) : ATTRIBUTS, MÉTHODES ET CONSTRUCTEUR
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 2 : Attributs et Méthodes en Profondeur
# - Chapitre 3 : Le Constructeur __init__ Avancé
# - Chapitre 4 : Méthodes Spéciales (Magic Methods)
#
# [TEMPS] TEMPS : ~4-6 heures
# [DOCS] PRÉREQUIS : Chapitre 0 et 1 complétés
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 2 : ATTRIBUTS ET MÉTHODES EN PROFONDEUR
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Différence attributs de classe vs d'instance
[OK] Attributs privés et protégés
[OK] Méthodes d'instance, de classe et statiques
[OK] Propriétés avec @property
[OK] Getters et setters
"""


# ----------------------------------------------------------------------------
# [PACKAGE] ATTRIBUTS DE CLASSE VS ATTRIBUTS D'INSTANCE
# ----------------------------------------------------------------------------

"""
ATTRIBUTS D'INSTANCE (Vus au Chapitre 1)
"""

class Voiture:
    def __init__(self, marque, modele):
        # Attributs d'instance (propres à chaque objet)
        self.marque = marque
        self.modele = modele

voiture1 = Voiture("Toyota", "Corolla")
voiture2 = Voiture("Honda", "Civic")

print(voiture1.marque)  # Toyota
print(voiture2.marque)  # Honda

"""
ATTRIBUTS DE CLASSE (Partagés par toutes les instances)
"""

class Voiture:
    # Attribut de classe (partagé par tous les objets)
    nombre_de_roues = 4
    fabricant_defaut = "Inconnu"
    
    def __init__(self, marque, modele):
        self.marque = marque
        self.modele = modele

# Accessible via la classe
print(Voiture.nombre_de_roues)  # 4

# Accessible via les instances
v1 = Voiture("Toyota", "Corolla")
v2 = Voiture("Honda", "Civic")

print(v1.nombre_de_roues)  # 4
print(v2.nombre_de_roues)  # 4

"""
[IDEE] DIFFÉRENCE FONDAMENTALE

ATTRIBUT DE CLASSE :
- Défini directement dans la classe
- UNE SEULE copie en mémoire
- Partagé par toutes les instances
- Modification affecte toutes les instances

ATTRIBUT D'INSTANCE :
- Défini dans __init__ avec self
- UNE copie PAR instance
- Propre à chaque objet
- Modification affecte seulement cet objet
"""

# Exemple démonstratif
class Compteur:
    # Attribut de classe
    total = 0
    
    def __init__(self, nom):
        # Attribut d'instance
        self.nom = nom
        self.valeur = 0
        
        # Incrémenter le compteur total (classe)
        Compteur.total += 1
    
    def incrementer(self):
        # Incrémenter compteur de cette instance
        self.valeur += 1

# Créer plusieurs compteurs
c1 = Compteur("Compteur 1")
c2 = Compteur("Compteur 2")
c3 = Compteur("Compteur 3")

print(f"Total de compteurs créés : {Compteur.total}")  # 3

c1.incrementer()
c1.incrementer()
c2.incrementer()

print(f"{c1.nom} : {c1.valeur}")  # Compteur 1 : 2
print(f"{c2.nom} : {c2.valeur}")  # Compteur 2 : 1
print(f"{c3.nom} : {c3.valeur}")  # Compteur 3 : 0

"""
[ATTENTION] PIÈGE CLASSIQUE : ATTRIBUTS MUTABLES DE CLASSE
"""

# [X] MAUVAIS : Liste comme attribut de classe
class MauvaiseClasse:
    elements = []  # Partagé par toutes les instances !
    
    def ajouter(self, element):
        self.elements.append(element)

obj1 = MauvaiseClasse()
obj2 = MauvaiseClasse()

obj1.ajouter("A")
obj2.ajouter("B")

print(obj1.elements)  # ['A', 'B'] <- Oups ! Partagé !
print(obj2.elements)  # ['A', 'B']

# [OK] BON : Liste comme attribut d'instance
class BonneClasse:
    def __init__(self):
        self.elements = []  # Chaque instance a sa propre liste
    
    def ajouter(self, element):
        self.elements.append(element)

obj1 = BonneClasse()
obj2 = BonneClasse()

obj1.ajouter("A")
obj2.ajouter("B")

print(obj1.elements)  # ['A']
print(obj2.elements)  # ['B'] <- Chacun a sa liste !

"""
[CLE] RÈGLE : QUAND UTILISER ATTRIBUT DE CLASSE ?

Utilisez attribut de classe pour :
[OK] Constantes partagées
[OK] Compteurs globaux
[OK] Configuration par défaut
[OK] Données immuables partagées

Exemple :
"""

class Configuration:
    # Constantes de classe
    VERSION = "1.0.0"
    MAX_CONNEXIONS = 100
    TIMEOUT = 30
    
    def __init__(self, nom_app):
        self.nom_app = nom_app  # Spécifique à chaque instance


# ----------------------------------------------------------------------------
# [VERROUILLE] ATTRIBUTS PRIVÉS ET PROTÉGÉS
# ----------------------------------------------------------------------------

"""
CONVENTION DE NOMMAGE PYTHON

Python n'a pas de vrai concept de "privé" comme Java/C++
Mais des CONVENTIONS :

1. PUBLIC : nom
   Accessible partout

2. PROTÉGÉ : _nom (un underscore)
   Convention : "Ne touche pas à ça (sauf si tu sais ce que tu fais)"

3. PRIVÉ : __nom (double underscore)
   Name mangling : Python "cache" vraiment l'attribut
"""

class Personne:
    def __init__(self, nom, age, salaire):
        # Public
        self.nom = nom
        
        # Protégé (convention, pas forcé)
        self._age = age
        
        # Privé (name mangling)
        self.__salaire = salaire
    
    def afficher_info(self):
        # Accessible dans la classe
        print(f"{self.nom}, {self._age} ans, {self.__salaire}€")

# Test
alice = Personne("Alice", 30, 50000)

# Public : OK
print(alice.nom)  # Alice

# Protégé : Accessible mais découragé
print(alice._age)  # 30 (mais ne devriez pas faire ça)

# Privé : Erreur !
# print(alice.__salaire)  # AttributeError

# Mais accessible via name mangling (à éviter !)
print(alice._Personne__salaire)  # 50000

alice.afficher_info()  # Alice, 30 ans, 50000€

"""
[IDEE] NAME MANGLING EXPLIQUÉ

Python transforme __attribut en _NomClasse__attribut

class MaClasse:
    def __init__(self):
        self.__prive = "secret"

obj = MaClasse()
# obj.__prive        # [X] Erreur
# obj._MaClasse__prive  # [OK] Accessible (mais ne faites pas ça !)


[REFLEXION] POURQUOI UTILISER _ ET __ ?

_ (protégé) :
- Signal aux autres développeurs : "Attention, API interne"
- Peut changer entre versions
- Utilisez avec précaution

__ (privé) :
- Éviter conflits de noms dans héritage
- Vraiment interne à la classe
- Rarement nécessaire en pratique


EXEMPLE PRATIQUE : COMPTE BANCAIRE
"""

class CompteBancaire:
    def __init__(self, numero, titulaire, solde_initial=0):
        self.numero = numero        # Public : numéro de compte
        self.titulaire = titulaire  # Public : nom du titulaire
        self.__solde = solde_initial  # Privé : solde (protégé !)
    
    def deposer(self, montant):
        """Déposer de l'argent"""
        if montant > 0:
            self.__solde += montant
            print(f"Dépôt de {montant}€. Nouveau solde : {self.__solde}€")
        else:
            print("Montant invalide")
    
    def retirer(self, montant):
        """Retirer de l'argent"""
        if montant > 0 and self.__solde >= montant:
            self.__solde -= montant
            print(f"Retrait de {montant}€. Nouveau solde : {self.__solde}€")
        else:
            print("Retrait impossible")
    
    def get_solde(self):
        """Consulter le solde (lecture seule)"""
        return self.__solde

# Utilisation
compte = CompteBancaire("123456", "Alice", 1000)

# Public : OK
print(compte.numero)     # 123456
print(compte.titulaire)  # Alice

# Privé : Erreur directe
# print(compte.__solde)  # AttributeError

# Mais accessible via méthode
print(compte.get_solde())  # 1000

# Impossible de faire solde négatif directement
# compte.__solde = -5000  # Crée un NOUVEL attribut (pas celui de la classe !)

compte.deposer(500)   # Dépôt de 500€. Nouveau solde : 1500€
compte.retirer(200)   # Retrait de 200€. Nouveau solde : 1300€

"""
[CLE] RÈGLE PRATIQUE

PUBLIC (nom) :
- Attributs/méthodes destinés aux utilisateurs
- API stable
- Exemples : nom, age, calculer_total()

PROTÉGÉ (_nom) :
- API interne, peut changer
- "Experts seulement"
- Exemples : _cache, _valider_donnees()

PRIVÉ (__nom) :
- Rarement nécessaire
- Éviter conflits dans héritage complexe
- Exemples : __solde, __cle_secrete
"""


# ----------------------------------------------------------------------------
# [SCENARIO] TYPES DE MÉTHODES
# ----------------------------------------------------------------------------

"""
PYTHON A 3 TYPES DE MÉTHODES :

1. Méthodes d'instance (vues au Chapitre 1)
2. Méthodes de classe (@classmethod)
3. Méthodes statiques (@staticmethod)
"""

# 1⃣ MÉTHODES D'INSTANCE (Déjà vues)

class Voiture:
    def __init__(self, marque):
        self.marque = marque
        self.vitesse = 0
    
    def accelerer(self):  # Méthode d'instance
        """Opère sur l'instance (self)"""
        self.vitesse += 10

v = Voiture("Toyota")
v.accelerer()  # Agit sur v


# 2⃣ MÉTHODES DE CLASSE (@classmethod)

class Personne:
    population = 0  # Attribut de classe
    
    def __init__(self, nom):
        self.nom = nom
        Personne.population += 1
    
    @classmethod
    def obtenir_population(cls):
        """
        [IDEE] MÉTHODE DE CLASSE
        
        - Décorée avec @classmethod
        - Premier param : cls (la classe, pas l'instance)
        - Opère sur la classe, pas sur une instance
        - Peut créer instances (factory)
        """
        return cls.population
    
    @classmethod
    def depuis_annee_naissance(cls, nom, annee_naissance):
        """
        Factory method : créer Personne depuis année de naissance
        """
        from datetime import datetime
        age = datetime.now().year - annee_naissance
        # Créer instance de la classe
        personne = cls(nom)
        personne.age = age
        return personne

# Utilisation méthode de classe
print(Personne.obtenir_population())  # 0 (aucune instance)

p1 = Personne("Alice")
p2 = Personne("Bob")

print(Personne.obtenir_population())  # 2

# Factory method
p3 = Personne.depuis_annee_naissance("Charlie", 1990)
print(f"{p3.nom} a {p3.age} ans")

"""
[IDEE] POURQUOI @classmethod ?

1. FACTORY METHODS (créer instances de manière alternative)
2. Accéder/modifier attributs de classe
3. Méthodes qui concernent la classe, pas une instance
"""


# 3⃣ MÉTHODES STATIQUES (@staticmethod)

class MathUtils:
    """Classe utilitaire pour fonctions mathématiques"""
    
    @staticmethod
    def est_pair(nombre):
        """
        [IDEE] MÉTHODE STATIQUE
        
        - Décorée avec @staticmethod
        - PAS de self, PAS de cls
        - Fonction normale dans la classe
        - Ne dépend ni de l'instance ni de la classe
        - Juste pour organisation
        """
        return nombre % 2 == 0
    
    @staticmethod
    def plus_grand(a, b):
        """Retourne le plus grand de deux nombres"""
        return a if a > b else b

# Utilisation (pas besoin d'instance !)
print(MathUtils.est_pair(4))     # True
print(MathUtils.est_pair(7))     # False
print(MathUtils.plus_grand(10, 5))  # 10

"""
[IDEE] POURQUOI @staticmethod ?

1. Organisation logique (grouper fonctions liées)
2. Pas d'accès nécessaire à instance ou classe
3. Alternative aux fonctions globales
"""


# COMPARAISON DES 3 TYPES

class Exemple:
    attribut_classe = "Classe"
    
    def __init__(self):
        self.attribut_instance = "Instance"
    
    # 1. Méthode d'instance
    def methode_instance(self):
        """Accède à self (l'instance)"""
        print(f"Instance : {self.attribut_instance}")
        print(f"Classe (via self) : {self.attribut_classe}")
    
    # 2. Méthode de classe
    @classmethod
    def methode_classe(cls):
        """Accède à cls (la classe)"""
        print(f"Classe : {cls.attribut_classe}")
        # print(f"Instance : {cls.attribut_instance}")  # [X] Erreur !
    
    # 3. Méthode statique
    @staticmethod
    def methode_statique():
        """N'accède ni à self ni à cls"""
        print("Je suis une méthode statique")
        # print(self.attribut_instance)  # [X] Erreur !
        # print(cls.attribut_classe)     # [X] Erreur !

# Test
obj = Exemple()

obj.methode_instance()
# Instance : Instance
# Classe (via self) : Classe

Exemple.methode_classe()
# Classe : Classe

Exemple.methode_statique()
# Je suis une méthode statique

"""
[CLE] TABLEAU RÉCAPITULATIF

┌──────────────┬─────────────────┬─────────────────┬────────────────────┐
│   Type       │   Décorateur    │  1er Param      │   Quand utiliser   │
├──────────────┼─────────────────┼─────────────────┼────────────────────┤
│  Instance    │  Aucun          │  self           │  Agir sur objet    │
│  Classe      │  @classmethod   │  cls            │  Agir sur classe   │
│  Statique    │  @staticmethod  │  Aucun          │  Fonction liée     │
└──────────────┴─────────────────┴─────────────────┴────────────────────┘
"""


# ----------------------------------------------------------------------------
# [LABEL] PROPRIÉTÉS AVEC @property
# ----------------------------------------------------------------------------

"""
PROBLÈME : VALIDATION ET CONTRÔLE D'ACCÈS

Sans propriétés :
"""

class Rectangle:
    def __init__(self, largeur, hauteur):
        self.largeur = largeur
        self.hauteur = hauteur

r = Rectangle(5, 10)
r.largeur = -3  # [X] Oups ! Largeur négative !

"""
[X] SOLUTION NAÏVE : Getters et setters manuels
"""

class Rectangle:
    def __init__(self, largeur, hauteur):
        self.__largeur = largeur
        self.__hauteur = hauteur
    
    def get_largeur(self):
        return self.__largeur
    
    def set_largeur(self, valeur):
        if valeur > 0:
            self.__largeur = valeur
        else:
            raise ValueError("Largeur doit être positive")

r = Rectangle(5, 10)
# r.largeur = 3  # [X] Erreur, attribut privé
r.set_largeur(3)  # Obligé d'utiliser setter (verbeux)
print(r.get_largeur())

"""
[OK] SOLUTION PYTHONIQUE : @property
"""

class Rectangle:
    def __init__(self, largeur, hauteur):
        # Utilise les setters pour validation
        self.largeur = largeur
        self.hauteur = hauteur
    
    @property
    def largeur(self):
        """
        [IDEE] GETTER via @property
        
        Permet de faire : r.largeur (comme attribut)
        au lieu de : r.get_largeur() (méthode)
        """
        return self.__largeur
    
    @largeur.setter
    def largeur(self, valeur):
        """
        [IDEE] SETTER via @nom.setter
        
        Permet de faire : r.largeur = 5 (comme attribut)
        au lieu de : r.set_largeur(5) (méthode)
        """
        if valeur > 0:
            self.__largeur = valeur
        else:
            raise ValueError("Largeur doit être positive")
    
    @property
    def hauteur(self):
        return self.__hauteur
    
    @hauteur.setter
    def hauteur(self, valeur):
        if valeur > 0:
            self.__hauteur = valeur
        else:
            raise ValueError("Hauteur doit être positive")
    
    @property
    def aire(self):
        """
        [IDEE] PROPRIÉTÉ CALCULÉE (lecture seule)
        
        Pas de setter : readonly
        """
        return self.__largeur * self.__hauteur

# Utilisation (syntaxe d'attribut !)
r = Rectangle(5, 10)

# Getter (lecture)
print(r.largeur)  # 5 (appelle la méthode getter)

# Setter (écriture avec validation)
r.largeur = 8     # (appelle la méthode setter)
print(r.largeur)  # 8

# Validation automatique
try:
    r.largeur = -3  # ValueError: Largeur doit être positive
except ValueError as e:
    print(f"Erreur : {e}")

# Propriété calculée (readonly)
print(r.aire)  # 80 (5 * 10)

# Impossible de modifier aire
# r.aire = 100  # AttributeError: can't set attribute

"""
[IDEE] AVANTAGES DE @property

1. SYNTAXE NATURELLE
   r.largeur = 5  vs  r.set_largeur(5)

2. VALIDATION AUTOMATIQUE
   Impossible de mettre valeur invalide

3. PROPRIÉTÉS CALCULÉES
   aire, perimetre calculés à la volée

4. ENCAPSULATION
   Attribut privé mais accès contrôlé

5. COMPATIBILITÉ
   Code existant ne change pas si vous ajoutez validation plus tard
"""


# EXEMPLE COMPLET : TEMPÉRATURE

class Temperature:
    """Classe pour gérer température avec conversion C/F"""
    
    def __init__(self, celsius=0):
        self.celsius = celsius  # Utilise le setter
    
    @property
    def celsius(self):
        """Getter Celsius"""
        return self.__celsius
    
    @celsius.setter
    def celsius(self, valeur):
        """Setter Celsius avec validation"""
        if valeur < -273.15:  # Zéro absolu
            raise ValueError("Température en dessous du zéro absolu !")
        self.__celsius = valeur
    
    @property
    def fahrenheit(self):
        """Propriété calculée Fahrenheit"""
        return self.__celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, valeur):
        """Setter Fahrenheit (converti en Celsius)"""
        self.celsius = (valeur - 32) * 5/9  # Utilise setter celsius
    
    @property
    def kelvin(self):
        """Propriété calculée Kelvin (readonly)"""
        return self.__celsius + 273.15

# Test
temp = Temperature(25)
print(f"{temp.celsius}°C = {temp.fahrenheit}°F = {temp.kelvin}K")
# 25°C = 77.0°F = 298.15K

temp.fahrenheit = 86
print(f"{temp.celsius}°C")  # 30.0°C

# Validation
try:
    temp.celsius = -300  # Sous zéro absolu
except ValueError as e:
    print(e)  # Température en dessous du zéro absolu !


# ============================================================================
# [GUIDE] CHAPITRE 3 : LE CONSTRUCTEUR __init__ AVANCÉ
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Paramètres par défaut dans __init__
[OK] *args et **kwargs
[OK] Validation dans __init__
[OK] Initialisation complexe
[OK] Factory methods
"""


# ----------------------------------------------------------------------------
# [DESIGN] PARAMÈTRES PAR DÉFAUT
# ----------------------------------------------------------------------------

"""
PARAMÈTRES OPTIONNELS
"""

class Personne:
    def __init__(self, nom, age=18, ville="Paris"):
        """
        [IDEE] PARAMÈTRES PAR DÉFAUT
        
        - nom : obligatoire
        - age : optionnel (défaut 18)
        - ville : optionnel (défaut "Paris")
        """
        self.nom = nom
        self.age = age
        self.ville = ville

# Différentes façons de créer
p1 = Personne("Alice")                    # age=18, ville="Paris"
p2 = Personne("Bob", 25)                  # ville="Paris"
p3 = Personne("Charlie", 30, "Lyon")
p4 = Personne("David", ville="Marseille") # age=18, ville="Marseille"

print(f"{p1.nom}, {p1.age} ans, {p1.ville}")
# Alice, 18 ans, Paris

"""
[ATTENTION] PIÈGE : VALEURS PAR DÉFAUT MUTABLES
"""

# [X] MAUVAIS
class MauvaiseClasse:
    def __init__(self, nom, liste=[]):
        """
        [X] DANGER ! liste=[] est partagée entre appels !
        """
        self.nom = nom
        self.liste = liste

obj1 = MauvaiseClasse("obj1")
obj2 = MauvaiseClasse("obj2")

obj1.liste.append("A")
print(obj2.liste)  # ['A'] <- Oups ! Partagé !

# [OK] BON
class BonneClasse:
    def __init__(self, nom, liste=None):
        """[OK] Utiliser None puis créer nouvelle liste"""
        self.nom = nom
        self.liste = liste if liste is not None else []

obj1 = BonneClasse("obj1")
obj2 = BonneClasse("obj2")

obj1.liste.append("A")
print(obj2.liste)  # [] <- Chacun a sa liste !

"""
[CLE] RÈGLE : Valeurs par défaut mutables

[X] def __init__(self, liste=[])     # Partagé !
[X] def __init__(self, dico={})      # Partagé !
[X] def __init__(self, obj=Objet())  # Partagé !

[OK] def __init__(self, liste=None)
       self.liste = liste or []

[OK] def __init__(self, dico=None):
       self.dico = dico if dico else {}
"""


# ----------------------------------------------------------------------------
# * *args ET **kwargs DANS __init__
# ----------------------------------------------------------------------------

"""
NOMBRE VARIABLE D'ARGUMENTS
"""

class Equipe:
    def __init__(self, nom, *membres):
        """
        [IDEE] *membres = Nombre variable d'arguments positionnels
        
        membres devient un tuple
        """
        self.nom = nom
        self.membres = list(membres)

# Utilisation
equipe1 = Equipe("Dev", "Alice", "Bob")
equipe2 = Equipe("Design", "Charlie", "David", "Eve")

print(equipe1.membres)  # ['Alice', 'Bob']
print(equipe2.membres)  # ['Charlie', 'David', 'Eve']

"""
KWARGS (Keyword Arguments)
"""

class Configuration:
    def __init__(self, nom, **options):
        """
        [IDEE] **options = Nombre variable d'arguments nommés
        
        options devient un dict
        """
        self.nom = nom
        self.options = options

# Utilisation
config1 = Configuration("App1", debug=True, port=8000, host="localhost")
config2 = Configuration("App2", timeout=30, max_users=100)

print(config1.options)
# {'debug': True, 'port': 8000, 'host': 'localhost'}

"""
COMBINAISON : args ET kwargs
"""

class Flexible:
    def __init__(self, obligatoire, *args, param_defaut=10, **kwargs):
        """
        Ordre des paramètres :
        1. Paramètres positionnels obligatoires
        2. *args (positionnels variables)
        3. Paramètres nommés avec défaut
        4. **kwargs (nommés variables)
        """
        self.obligatoire = obligatoire
        self.args = args
        self.param_defaut = param_defaut
        self.kwargs = kwargs

obj = Flexible(
    "requis",
    "extra1", "extra2",
    param_defaut=20,
    option1="valeur1",
    option2="valeur2"
)

print(obj.obligatoire)    # requis
print(obj.args)           # ('extra1', 'extra2')
print(obj.param_defaut)   # 20
print(obj.kwargs)         # {'option1': 'valeur1', 'option2': 'valeur2'}


# ----------------------------------------------------------------------------
# [OK] VALIDATION DANS __init__
# ----------------------------------------------------------------------------

"""
VALIDER LES DONNÉES DÈS LA CRÉATION
"""

class Personne:
    def __init__(self, nom, age, email):
        # Validation nom
        if not nom or not isinstance(nom, str):
            raise ValueError("Nom doit être une chaîne non vide")
        
        # Validation age
        if not isinstance(age, int) or age < 0 or age > 150:
            raise ValueError("Âge doit être entre 0 et 150")
        
        # Validation email
        if "@" not in email:
            raise ValueError("Email invalide")
        
        # Si tout OK, créer attributs
        self.nom = nom
        self.age = age
        self.email = email

# Test validation
try:
    p1 = Personne("Alice", 25, "alice@example.com")  # [OK] OK
    print(f"{p1.nom} créé")
    
    p2 = Personne("", 25, "bob@example.com")  # [X] Nom vide
except ValueError as e:
    print(f"Erreur : {e}")

try:
    p3 = Personne("Charlie", 200, "charlie@example.com")  # [X] Âge invalide
except ValueError as e:
    print(f"Erreur : {e}")

"""
MÉTHODE DE VALIDATION SÉPARÉE (Plus propre)
"""

class Personne:
    def __init__(self, nom, age, email):
        self.nom = self._valider_nom(nom)
        self.age = self._valider_age(age)
        self.email = self._valider_email(email)
    
    def _valider_nom(self, nom):
        """Valide et retourne nom"""
        if not nom or not isinstance(nom, str):
            raise ValueError("Nom invalide")
        return nom.strip()  # Enlever espaces
    
    def _valider_age(self, age):
        """Valide et retourne age"""
        if not isinstance(age, int) or not (0 <= age <= 150):
            raise ValueError("Âge doit être entre 0 et 150")
        return age
    
    def _valider_email(self, email):
        """Valide et retourne email"""
        if "@" not in email or "." not in email:
            raise ValueError("Email invalide")
        return email.lower()  # Normaliser en minuscules

p = Personne("  Alice  ", 25, "ALICE@EXAMPLE.COM")
print(f"{p.nom} - {p.email}")  # Alice - alice@example.com


# Ce fichier continue...
# ============================================================================

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 1 (FIN) : MÉTHODES SPÉCIALES ET RÉCAPITULATIF
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 4 : Méthodes Spéciales (Magic Methods) - Complet
# - Exercices Pratiques
# - Récapitulatif Partie 1
#
# [TEMPS] TEMPS : ~3-4 heures
# [DOCS] PRÉREQUIS : Chapitres 1, 2 et 3 complétés
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 4 : MÉTHODES SPÉCIALES (MAGIC METHODS)
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce que les méthodes spéciales (dunder methods)
[OK] Utiliser __str__ et __repr__
[OK] Surcharger opérateurs (+, -, *, ==, <, etc.)
[OK] Gérer longueur et accès par index
[OK] Rendre objets itérables
[OK] Context managers avec __enter__ et __exit__
"""


# ----------------------------------------------------------------------------
# [TOP_HAT] QU'EST-CE QUE LES MÉTHODES SPÉCIALES ?
# ----------------------------------------------------------------------------

"""
MÉTHODES SPÉCIALES (Magic Methods / Dunder Methods)

[IDEE] Définition :
- Méthodes avec __ (double underscore) au début ET à la fin
- Appelées automatiquement par Python dans certaines situations
- Permettent de personnaliser comportement de vos objets
- "Dunder" = Double UNDERscore


EXEMPLES DÉJÀ VUS :

__init__(self, ...)
    Appelé lors de création : obj = MaClasse()

__str__(self)
    Appelé par print() et str()

__repr__(self)
    Appelé par repr() et dans l'interpréteur


LISTE COMPLÈTE DES MÉTHODES SPÉCIALES :

Construction et Représentation :
__init__, __new__, __del__
__str__, __repr__, __format__

Comparaison :
__eq__, __ne__, __lt__, __le__, __gt__, __ge__

Opérateurs Arithmétiques :
__add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__

Opérateurs Augmentés :
__iadd__, __isub__, __imul__, etc.

Collections :
__len__, __getitem__, __setitem__, __delitem__
__contains__, __iter__, __next__

Context Managers :
__enter__, __exit__

Et bien d'autres...
"""


# ----------------------------------------------------------------------------
# [FICHIER] __str__ ET __repr__ : REPRÉSENTATION D'OBJETS
# ----------------------------------------------------------------------------

"""
SANS __str__ ET __repr__
"""

class Personne:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age

p = Personne("Alice", 30)
print(p)  # <__main__.Personne object at 0x...>
# Pas très utile !

"""
AVEC __str__ (Pour l'utilisateur final)
"""

class Personne:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
    
    def __str__(self):
        """
        [IDEE] __str__ : Représentation "jolie" pour l'utilisateur
        
        Appelé par :
        - print(obj)
        - str(obj)
        - f"{obj}"
        
        Doit retourner string lisible
        """
        return f"{self.nom}, {self.age} ans"

p = Personne("Alice", 30)
print(p)         # Alice, 30 ans
print(str(p))    # Alice, 30 ans
print(f"Info: {p}")  # Info: Alice, 30 ans

"""
AVEC __repr__ (Pour les développeurs)
"""

class Personne:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
    
    def __repr__(self):
        """
        [IDEE] __repr__ : Représentation "officielle" pour les développeurs
        
        Appelé par :
        - repr(obj)
        - Dans l'interpréteur interactif
        - Dans les conteneurs (listes, etc.)
        
        Devrait idéalement permettre de recréer l'objet :
        eval(repr(obj)) == obj
        """
        return f"Personne(nom='{self.nom}', age={self.age})"

p = Personne("Alice", 30)
print(repr(p))  # Personne(nom='Alice', age=30)

# Dans l'interpréteur :
# >>> p
# Personne(nom='Alice', age=30)

# Dans une liste :
personnes = [Personne("Alice", 30), Personne("Bob", 25)]
print(personnes)
# [Personne(nom='Alice', age=30), Personne(nom='Bob', age=25)]

"""
AVEC LES DEUX : __str__ ET __repr__
"""

class Personne:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
    
    def __str__(self):
        """Pour print() - lisible pour utilisateur"""
        return f"{self.nom}, {self.age} ans"
    
    def __repr__(self):
        """Pour repr() - utile pour développeur"""
        return f"Personne('{self.nom}', {self.age})"

p = Personne("Alice", 30)

print(p)         # Alice, 30 ans (utilise __str__)
print(repr(p))   # Personne('Alice', 30) (utilise __repr__)

# Si __str__ absent, __repr__ est utilisé
# Si __repr__ absent, fallback sur <__main__.Personne object at 0x...>

"""
[CLE] RÈGLES :

__str__ :
- Pour affichage utilisateur final
- Lisible et "joli"
- Informel

__repr__ :
- Pour développeurs et debugging
- Non-ambigu
- Idéalement : eval(repr(obj)) recrée l'objet
- Si vous n'implémentez qu'un seul : faites __repr__


EXEMPLE COMPLET : COMPTE BANCAIRE
"""

class CompteBancaire:
    def __init__(self, numero, titulaire, solde):
        self.numero = numero
        self.titulaire = titulaire
        self.solde = solde
    
    def __str__(self):
        """Affichage pour l'utilisateur"""
        return f"Compte {self.numero} - {self.titulaire}: {self.solde}€"
    
    def __repr__(self):
        """Affichage pour le développeur"""
        return f"CompteBancaire('{self.numero}', '{self.titulaire}', {self.solde})"

compte = CompteBancaire("123456", "Alice", 1000.0)

print(compte)
# Compte 123456 - Alice: 1000.0€

print(repr(compte))
# CompteBancaire('123456', 'Alice', 1000.0)

# Dans une liste
comptes = [
    CompteBancaire("123", "Alice", 1000),
    CompteBancaire("456", "Bob", 500)
]
print(comptes)
# [CompteBancaire('123', 'Alice', 1000), CompteBancaire('456', 'Bob', 500)]


# ----------------------------------------------------------------------------
# + SURCHARGE D'OPÉRATEURS ARITHMÉTIQUES
# ----------------------------------------------------------------------------

"""
OPÉRATEURS PAR DÉFAUT NE FONCTIONNENT PAS
"""

class Vecteur:
    def __init__(self, x, y):
        self.x = x
        self.y = y

v1 = Vecteur(1, 2)
v2 = Vecteur(3, 4)

# v3 = v1 + v2  # [X] TypeError: unsupported operand type(s)

"""
SURCHARGER L'OPÉRATEUR + AVEC __add__
"""

class Vecteur:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """
        [IDEE] __add__ : Surcharge opérateur +
        
        v1 + v2  ->  v1.__add__(v2)
        """
        return Vecteur(self.x + other.x, self.y + other.y)
    
    def __str__(self):
        return f"Vecteur({self.x}, {self.y})"

v1 = Vecteur(1, 2)
v2 = Vecteur(3, 4)

v3 = v1 + v2  # Maintenant ça marche !
print(v3)     # Vecteur(4, 6)

"""
AUTRES OPÉRATEURS ARITHMÉTIQUES
"""

class Vecteur:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """Addition : +"""
        return Vecteur(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        """Soustraction : -"""
        return Vecteur(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        """Multiplication par scalaire : *"""
        if isinstance(scalar, (int, float)):
            return Vecteur(self.x * scalar, self.y * scalar)
        raise TypeError("Multiplication seulement par nombre")
    
    def __truediv__(self, scalar):
        """Division : /"""
        if isinstance(scalar, (int, float)):
            return Vecteur(self.x / scalar, self.y / scalar)
        raise TypeError("Division seulement par nombre")
    
    def __neg__(self):
        """Négation : -v"""
        return Vecteur(-self.x, -self.y)
    
    def __str__(self):
        return f"V({self.x}, {self.y})"

# Tests
v1 = Vecteur(3, 4)
v2 = Vecteur(1, 2)

print(v1 + v2)   # V(4, 6)
print(v1 - v2)   # V(2, 2)
print(v1 * 2)    # V(6, 8)
print(v1 / 2)    # V(1.5, 2.0)
print(-v1)       # V(-3, -4)

"""
[IDEE] TABLEAU DES OPÉRATEURS ARITHMÉTIQUES

┌────────────┬──────────────┬───────────────────────┐
│  Opérateur │   Méthode    │      Exemple          │
├────────────┼──────────────┼───────────────────────┤
│     +      │  __add__     │  a + b                │
│     -      │  __sub__     │  a - b                │
│     *      │  __mul__     │  a * b                │
│     /      │  __truediv__ │  a / b                │
│     //     │  __floordiv__│  a // b               │
│     %      │  __mod__     │  a % b                │
│     **     │  __pow__     │  a ** b               │
│     -x     │  __neg__     │  -a                   │
│     +x     │  __pos__     │  +a                   │
│    abs(x)  │  __abs__     │  abs(a)               │
└────────────┴──────────────┴───────────────────────┘


OPÉRATEURS AUGMENTÉS (In-Place)
"""

class Compteur:
    def __init__(self, valeur=0):
        self.valeur = valeur
    
    def __iadd__(self, other):
        """
        [IDEE] __iadd__ : Opérateur +=
        
        c += 5  ->  c.__iadd__(5)
        
        Doit MODIFIER l'objet et RETOURNER self
        """
        self.valeur += other
        return self  # <- Important !
    
    def __isub__(self, other):
        """Opérateur -="""
        self.valeur -= other
        return self
    
    def __str__(self):
        return f"Compteur({self.valeur})"

c = Compteur(10)
print(c)      # Compteur(10)

c += 5
print(c)      # Compteur(15)

c -= 3
print(c)      # Compteur(12)

"""
[IDEE] DIFFÉRENCE : __add__ vs __iadd__

__add__ : Crée NOUVEAU objet
"""
class A:
    def __add__(self, other):
        return A()  # Nouveau

a = A()
b = a + 5  # a reste inchangé, b = nouvel objet

"""
__iadd__ : Modifie objet EXISTANT
"""
class B:
    def __iadd__(self, other):
        # Modifier self
        return self  # Même objet

b = B()
b += 5  # b est modifié sur place


# ----------------------------------------------------------------------------
# [SCALES] SURCHARGE D'OPÉRATEURS DE COMPARAISON
# ----------------------------------------------------------------------------

"""
COMPARER DES OBJETS
"""

class Personne:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
    
    def __eq__(self, other):
        """
        [IDEE] __eq__ : Opérateur ==
        
        p1 == p2  ->  p1.__eq__(p2)
        """
        if not isinstance(other, Personne):
            return False
        return self.nom == other.nom and self.age == other.age
    
    def __ne__(self, other):
        """Opérateur !="""
        return not self.__eq__(other)
    
    def __lt__(self, other):
        """
        [IDEE] __lt__ : Opérateur <
        
        Comparer par âge
        """
        return self.age < other.age
    
    def __le__(self, other):
        """Opérateur <="""
        return self.age <= other.age
    
    def __gt__(self, other):
        """Opérateur >"""
        return self.age > other.age
    
    def __ge__(self, other):
        """Opérateur >="""
        return self.age >= other.age
    
    def __str__(self):
        return f"{self.nom} ({self.age} ans)"

# Tests
p1 = Personne("Alice", 30)
p2 = Personne("Bob", 25)
p3 = Personne("Alice", 30)

# Égalité
print(p1 == p3)  # True (même nom et âge)
print(p1 == p2)  # False
print(p1 != p2)  # True

# Comparaison (par âge)
print(p1 > p2)   # True (30 > 25)
print(p2 < p1)   # True (25 < 30)
print(p1 >= p3)  # True (30 >= 30)

# Tri possible maintenant !
personnes = [
    Personne("Charlie", 35),
    Personne("Alice", 30),
    Personne("Bob", 25)
]

personnes_triees = sorted(personnes)
for p in personnes_triees:
    print(p)
# Bob (25 ans)
# Alice (30 ans)
# Charlie (35 ans)

"""
[IDEE] TABLEAU DES OPÉRATEURS DE COMPARAISON

┌────────────┬──────────────┬───────────────────────┐
│  Opérateur │   Méthode    │      Exemple          │
├────────────┼──────────────┼───────────────────────┤
│     ==     │  __eq__      │  a == b               │
│     !=     │  __ne__      │  a != b               │
│     <      │  __lt__      │  a < b                │
│     <=     │  __le__      │  a <= b               │
│     >      │  __gt__      │  a > b                │
│     >=     │  __ge__      │  a >= b               │
└────────────┴──────────────┴───────────────────────┘


[ATTENTION] NOTE : Si vous implémentez __eq__, pensez à __hash__ aussi
(pour utiliser objets dans sets et comme clés de dict)
"""

class Personne:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
    
    def __eq__(self, other):
        if not isinstance(other, Personne):
            return False
        return self.nom == other.nom and self.age == other.age
    
    def __hash__(self):
        """
        [IDEE] __hash__ : Pour utiliser dans set/dict
        
        Doit retourner un int
        Objets égaux (__eq__) doivent avoir même hash
        """
        return hash((self.nom, self.age))

# Maintenant peut être utilisé dans set
personnes = {
    Personne("Alice", 30),
    Personne("Bob", 25),
    Personne("Alice", 30)  # Doublon (ignoré grâce à __eq__ et __hash__)
}
print(len(personnes))  # 2


# ----------------------------------------------------------------------------
# [MESURE] COLLECTIONS : __len__, __getitem__, __setitem__
# ----------------------------------------------------------------------------

"""
RENDRE OBJET "MESURABLE" AVEC __len__
"""

class Equipe:
    def __init__(self, nom):
        self.nom = nom
        self.membres = []
    
    def ajouter(self, membre):
        self.membres.append(membre)
    
    def __len__(self):
        """
        [IDEE] __len__ : Retourne longueur
        
        len(obj)  ->  obj.__len__()
        """
        return len(self.membres)

equipe = Equipe("Dev")
equipe.ajouter("Alice")
equipe.ajouter("Bob")

print(len(equipe))  # 2 (appelle __len__)

"""
ACCÈS PAR INDEX AVEC __getitem__
"""

class Equipe:
    def __init__(self, nom):
        self.nom = nom
        self.membres = []
    
    def ajouter(self, membre):
        self.membres.append(membre)
    
    def __len__(self):
        return len(self.membres)
    
    def __getitem__(self, index):
        """
        [IDEE] __getitem__ : Accès par index
        
        obj[index]  ->  obj.__getitem__(index)
        
        Permet :
        - obj[0], obj[1], ...
        - obj[-1] (dernier)
        - obj[1:3] (slicing)
        - for item in obj (itération)
        """
        return self.membres[index]

equipe = Equipe("Dev")
equipe.ajouter("Alice")
equipe.ajouter("Bob")
equipe.ajouter("Charlie")

# Accès par index
print(equipe[0])   # Alice
print(equipe[-1])  # Charlie

# Slicing
print(equipe[0:2])  # ['Alice', 'Bob']

# Itération possible !
for membre in equipe:
    print(membre)
# Alice
# Bob
# Charlie

"""
MODIFICATION PAR INDEX AVEC __setitem__
"""

class Equipe:
    def __init__(self, nom):
        self.nom = nom
        self.membres = []
    
    def ajouter(self, membre):
        self.membres.append(membre)
    
    def __getitem__(self, index):
        return self.membres[index]
    
    def __setitem__(self, index, valeur):
        """
        [IDEE] __setitem__ : Modification par index
        
        obj[index] = valeur  ->  obj.__setitem__(index, valeur)
        """
        self.membres[index] = valeur
    
    def __delitem__(self, index):
        """
        [IDEE] __delitem__ : Suppression par index
        
        del obj[index]  ->  obj.__delitem__(index)
        """
        del self.membres[index]

equipe = Equipe("Dev")
equipe.ajouter("Alice")
equipe.ajouter("Bob")

# Modification
equipe[1] = "Robert"
print(equipe[1])  # Robert

# Suppression
del equipe[0]
print(len(equipe))  # 1

"""
VÉRIFIER APPARTENANCE AVEC __contains__
"""

class Equipe:
    def __init__(self, nom):
        self.nom = nom
        self.membres = []
    
    def ajouter(self, membre):
        self.membres.append(membre)
    
    def __contains__(self, membre):
        """
        [IDEE] __contains__ : Opérateur in
        
        item in obj  ->  obj.__contains__(item)
        """
        return membre in self.membres

equipe = Equipe("Dev")
equipe.ajouter("Alice")
equipe.ajouter("Bob")

print("Alice" in equipe)    # True
print("Charlie" in equipe)  # False


# ----------------------------------------------------------------------------
# [SYNC] RENDRE OBJETS ITÉRABLES
# ----------------------------------------------------------------------------

"""
ITÉRABLE AVEC __iter__ ET __next__
"""

class Compteur:
    def __init__(self, debut, fin):
        self.debut = debut
        self.fin = fin
        self.actuel = debut
    
    def __iter__(self):
        """
        [IDEE] __iter__ : Retourne itérateur
        
        for item in obj  ->  iter(obj).__next__()
        
        Retourne self si l'objet est lui-même l'itérateur
        """
        return self
    
    def __next__(self):
        """
        [IDEE] __next__ : Retourne élément suivant
        
        Lève StopIteration quand fini
        """
        if self.actuel > self.fin:
            raise StopIteration
        
        valeur = self.actuel
        self.actuel += 1
        return valeur

# Utilisation
compteur = Compteur(1, 5)

for nombre in compteur:
    print(nombre)
# 1
# 2
# 3
# 4
# 5

"""
[ATTENTION] PROBLÈME : Itérateur consommé une seule fois
"""

compteur = Compteur(1, 3)

for i in compteur:
    print(i)  # 1, 2, 3

# Deuxième itération : vide !
for i in compteur:
    print(i)  # Rien (déjà consommé)

"""
[OK] SOLUTION : __iter__ retourne NOUVEL itérateur
"""

class CompteurReutilisable:
    def __init__(self, debut, fin):
        self.debut = debut
        self.fin = fin
    
    def __iter__(self):
        """Retourne NOUVEAU itérateur à chaque fois"""
        return CompteurIterateur(self.debut, self.fin)

class CompteurIterateur:
    def __init__(self, debut, fin):
        self.actuel = debut
        self.fin = fin
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.actuel > self.fin:
            raise StopIteration
        valeur = self.actuel
        self.actuel += 1
        return valeur

# Test
compteur = CompteurReutilisable(1, 3)

for i in compteur:
    print(i)  # 1, 2, 3

for i in compteur:
    print(i)  # 1, 2, 3 (fonctionne à nouveau !)


# ----------------------------------------------------------------------------
# [SORTIE] CONTEXT MANAGERS : __enter__ ET __exit__
# ----------------------------------------------------------------------------

"""
PROBLÈME : GESTION DE RESSOURCES

Sans context manager :
"""

fichier = open("test.txt", "w")
fichier.write("Hello")
# Oups ! Oubli de fermer
# Ou erreur avant fermeture -> fichier reste ouvert

"""
AVEC with : Context Manager
"""

with open("test.txt", "w") as fichier:
    fichier.write("Hello")
# Fichier automatiquement fermé à la sortie du bloc

"""
CRÉER SON PROPRE CONTEXT MANAGER
"""

class GestionnaireConnexion:
    def __init__(self, nom):
        self.nom = nom
        self.connexion = None
    
    def __enter__(self):
        """
        [IDEE] __enter__ : Exécuté à l'entrée du with
        
        with obj as variable:
            │          │
            │          └─ Valeur retournée par __enter__
            └──────────── Appelle obj.__enter__()
        
        Retourne ce qui sera assigné à 'as variable'
        """
        print(f"Connexion à {self.nom}")
        self.connexion = f"Connexion-{self.nom}"
        return self.connexion
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """
        [IDEE] __exit__ : Exécuté à la sortie du with
        
        Toujours exécuté, même si erreur dans le bloc !
        
        Paramètres :
        - exc_type : Type de l'exception (None si pas d'erreur)
        - exc_val : Valeur de l'exception
        - exc_tb : Traceback
        
        Retourner True : Supprimer l'exception
        Retourner False/None : Propager l'exception
        """
        print(f"Fermeture de {self.nom}")
        self.connexion = None
        
        if exc_type is not None:
            print(f"Erreur capturée : {exc_val}")
        
        # Retourner False pour propager l'exception
        return False

# Utilisation
with GestionnaireConnexion("DB") as conn:
    print(f"Utilisation de {conn}")
    # Code ici

# Sortie :
# Connexion à DB
# Utilisation de Connexion-DB
# Fermeture de DB

"""
AVEC GESTION D'ERREUR
"""

try:
    with GestionnaireConnexion("DB") as conn:
        print(f"Utilisation de {conn}")
        raise ValueError("Erreur test")
except ValueError:
    print("Exception propagée")

# Sortie :
# Connexion à DB
# Utilisation de Connexion-DB
# Erreur capturée : Erreur test
# Fermeture de DB
# Exception propagée

"""
EXEMPLE PRATIQUE : TIMER
"""

import time

class Timer:
    """Context manager pour mesurer temps d'exécution"""
    
    def __enter__(self):
        self.debut = time.time()
        return self
    
    def __exit__(self, *args):
        self.fin = time.time()
        self.duree = self.fin - self.debut
        print(f"Temps d'exécution : {self.duree:.4f} secondes")
        return False

# Utilisation
with Timer():
    # Code à chronométrer
    total = sum(range(1000000))

# Sortie : Temps d'exécution : 0.0234 secondes

"""
EXEMPLE : GESTIONNAIRE DE TRANSACTION
"""

class Transaction:
    """Simule une transaction de base de données"""
    
    def __init__(self, nom):
        self.nom = nom
    
    def __enter__(self):
        print(f"Début transaction : {self.nom}")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            print(f"COMMIT transaction : {self.nom}")
        else:
            print(f"ROLLBACK transaction : {self.nom}")
            print(f"Raison : {exc_val}")
        return False  # Propager l'exception

# Transaction réussie
with Transaction("Virement"):
    print("Débit compte A")
    print("Crédit compte B")
# Sortie :
# Début transaction : Virement
# Débit compte A
# Crédit compte B
# COMMIT transaction : Virement

# Transaction échouée
try:
    with Transaction("Virement2"):
        print("Débit compte A")
        raise Exception("Solde insuffisant")
        print("Crédit compte B")  # Pas exécuté
except Exception:
    print("Transaction annulée")
# Sortie :
# Début transaction : Virement2
# Débit compte A
# ROLLBACK transaction : Virement2
# Raison : Solde insuffisant
# Transaction annulée


# ----------------------------------------------------------------------------
# [LISTE] RÉSUMÉ DES MÉTHODES SPÉCIALES IMPORTANTES
# ----------------------------------------------------------------------------

"""
[CLE] TABLEAU RÉCAPITULATIF

CONSTRUCTION ET REPRÉSENTATION
┌──────────────┬───────────────────────────────────────┐
│   Méthode    │          Utilisation                  │
├──────────────┼───────────────────────────────────────┤
│  __init__    │  Constructeur                         │
│  __del__     │  Destructeur (rarement utilisé)       │
│  __str__     │  print(obj), str(obj)                 │
│  __repr__    │  repr(obj), dans interpréteur         │
└──────────────┴───────────────────────────────────────┘

COMPARAISON
┌──────────────┬───────────────────────────────────────┐
│  __eq__      │  ==                                   │
│  __ne__      │  !=                                   │
│  __lt__      │  <                                    │
│  __le__      │  <=                                   │
│  __gt__      │  >                                    │
│  __ge__      │  >=                                   │
│  __hash__    │  hash(obj) - pour set/dict            │
└──────────────┴───────────────────────────────────────┘

OPÉRATEURS ARITHMÉTIQUES
┌──────────────┬───────────────────────────────────────┐
│  __add__     │  +                                    │
│  __sub__     │  -                                    │
│  __mul__     │  *                                    │
│  __truediv__ │  /                                    │
│  __floordiv__│  //                                   │
│  __mod__     │  %                                    │
│  __pow__     │  **                                   │
└──────────────┴───────────────────────────────────────┘

OPÉRATEURS AUGMENTÉS
┌──────────────┬───────────────────────────────────────┐
│  __iadd__    │  +=                                   │
│  __isub__    │  -=                                   │
│  __imul__    │  *=                                   │
│  __itruediv__│  /=                                   │
└──────────────┴───────────────────────────────────────┘

COLLECTIONS
┌──────────────┬───────────────────────────────────────┐
│  __len__     │  len(obj)                             │
│  __getitem__ │  obj[key]                             │
│  __setitem__ │  obj[key] = value                     │
│  __delitem__ │  del obj[key]                         │
│  __contains__│  item in obj                          │
│  __iter__    │  for item in obj                      │
│  __next__    │  next(obj)                            │
└──────────────┴───────────────────────────────────────┘

CONTEXT MANAGERS
┌──────────────┬───────────────────────────────────────┐
│  __enter__   │  with obj as x:                       │
│  __exit__    │  Sortie du with                       │
└──────────────┴───────────────────────────────────────┘

APPEL ET ATTRIBUTS
┌──────────────┬───────────────────────────────────────┐
│  __call__    │  obj() - objet appelable              │
│  __getattr__ │  obj.attribut inexistant              │
│  __setattr__ │  obj.attribut = value                 │
│  __delattr__ │  del obj.attribut                     │
└──────────────┴───────────────────────────────────────┘
"""


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 2 : CLASSE FRACTION
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer une classe Fraction complète

SPÉCIFICATIONS :

1. Attributs :
   - numerateur : int
   - denominateur : int

2. Méthodes spéciales :
   - __init__(numerateur, denominateur)
   - __str__ : Affichage "3/4"
   - __repr__ : "Fraction(3, 4)"
   - __add__ : Addition de fractions
   - __sub__ : Soustraction de fractions
   - __mul__ : Multiplication de fractions
   - __truediv__ : Division de fractions
   - __eq__ : Égalité de fractions
   - __lt__ : Comparaison <
   - __float__ : Conversion en float

3. Méthode :
   - simplifier() : Réduire la fraction


SOLUTION :
"""

from math import gcd

class Fraction:
    """Classe représentant une fraction mathématique"""
    
    def __init__(self, numerateur, denominateur=1):
        """
        Initialise une fraction
        
        Args:
            numerateur (int): Numérateur
            denominateur (int): Dénominateur (défaut 1)
        
        Raises:
            ValueError: Si dénominateur est 0
        """
        if denominateur == 0:
            raise ValueError("Dénominateur ne peut pas être 0")
        
        # Normaliser le signe (dénominateur toujours positif)
        if denominateur < 0:
            numerateur = -numerateur
            denominateur = -denominateur
        
        self.numerateur = numerateur
        self.denominateur = denominateur
        self.simplifier()
    
    def simplifier(self):
        """Simplifie la fraction (PGCD)"""
        pgcd = gcd(abs(self.numerateur), abs(self.denominateur))
        self.numerateur //= pgcd
        self.denominateur //= pgcd
    
    def __str__(self):
        """Affichage utilisateur : 3/4"""
        if self.denominateur == 1:
            return str(self.numerateur)
        return f"{self.numerateur}/{self.denominateur}"
    
    def __repr__(self):
        """Affichage développeur : Fraction(3, 4)"""
        return f"Fraction({self.numerateur}, {self.denominateur})"
    
    def __add__(self, other):
        """
        Addition de fractions
        a/b + c/d = (a*d + b*c) / (b*d)
        """
        if isinstance(other, int):
            other = Fraction(other)
        
        num = self.numerateur * other.denominateur + other.numerateur * self.denominateur
        den = self.denominateur * other.denominateur
        return Fraction(num, den)
    
    def __sub__(self, other):
        """Soustraction de fractions"""
        if isinstance(other, int):
            other = Fraction(other)
        
        num = self.numerateur * other.denominateur - other.numerateur * self.denominateur
        den = self.denominateur * other.denominateur
        return Fraction(num, den)
    
    def __mul__(self, other):
        """
        Multiplication de fractions
        a/b * c/d = (a*c) / (b*d)
        """
        if isinstance(other, int):
            other = Fraction(other)
        
        num = self.numerateur * other.numerateur
        den = self.denominateur * other.denominateur
        return Fraction(num, den)
    
    def __truediv__(self, other):
        """
        Division de fractions
        a/b / c/d = (a*d) / (b*c)
        """
        if isinstance(other, int):
            other = Fraction(other)
        
        num = self.numerateur * other.denominateur
        den = self.denominateur * other.numerateur
        return Fraction(num, den)
    
    def __eq__(self, other):
        """Égalité de fractions"""
        if isinstance(other, int):
            other = Fraction(other)
        
        return (self.numerateur == other.numerateur and 
                self.denominateur == other.denominateur)
    
    def __lt__(self, other):
        """Comparaison < (pour tri)"""
        if isinstance(other, int):
            other = Fraction(other)
        
        # Comparer en mettant au même dénominateur
        return (self.numerateur * other.denominateur < 
                other.numerateur * self.denominateur)
    
    def __float__(self):
        """Conversion en float"""
        return self.numerateur / self.denominateur
    
    def __int__(self):
        """Conversion en int (partie entière)"""
        return self.numerateur // self.denominateur

# Tests
f1 = Fraction(3, 4)
f2 = Fraction(1, 2)

print(f1)           # 3/4
print(repr(f1))     # Fraction(3, 4)

# Addition
f3 = f1 + f2
print(f3)           # 5/4

# Soustraction
f4 = f1 - f2
print(f4)           # 1/4

# Multiplication
f5 = f1 * f2
print(f5)           # 3/8

# Division
f6 = f1 / f2
print(f6)           # 3/2

# Avec entier
f7 = f1 + 2
print(f7)           # 11/4

# Comparaison
print(f1 == Fraction(6, 8))  # True (simplifiées égales)
print(f1 < f2)               # False
print(f1 > f2)               # True

# Conversion
print(float(f1))    # 0.75
print(int(f1))      # 0

# Simplification automatique
f8 = Fraction(6, 8)
print(f8)           # 3/4 (simplifié)


# ============================================================================
# [DOCS] RÉCAPITULATIF PARTIE 1 COMPLÈTE
# ============================================================================

"""
[BRAVO] FÉLICITATIONS ! PARTIE 1 TERMINÉE !

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 0 : Introduction à la POO
[OK] Qu'est-ce que la POO
[OK] POO vs Programmation Procédurale
[OK] Les 4 piliers (Encapsulation, Héritage, Polymorphisme, Abstraction)
[OK] Quand utiliser la POO

Chapitre 1 : Premières Classes
[OK] Créer une classe simple
[OK] Instancier des objets
[OK] Le mot-clé self
[OK] Attributs et méthodes d'instance
[OK] __init__ : le constructeur

Chapitre 2 : Attributs et Méthodes en Profondeur
[OK] Attributs de classe vs d'instance
[OK] Attributs privés (__) et protégés (_)
[OK] Méthodes d'instance
[OK] @classmethod (méthodes de classe)
[OK] @staticmethod (méthodes statiques)
[OK] @property (getters/setters pythoniques)

Chapitre 3 : Le Constructeur __init__ Avancé
[OK] Paramètres par défaut
[OK] *args et **kwargs
[OK] Validation dans __init__
[OK] Pièges des valeurs mutables

Chapitre 4 : Méthodes Spéciales
[OK] __str__ et __repr__
[OK] Surcharge opérateurs arithmétiques (+, -, *, /)
[OK] Surcharge opérateurs de comparaison (==, <, >, etc.)
[OK] Opérateurs augmentés (+=, -=, etc.)
[OK] Collections (__len__, __getitem__, __setitem__)
[OK] Itérateurs (__iter__, __next__)
[OK] Context managers (__enter__, __exit__)


[CLE] COMPÉTENCES ACQUISES

Vous pouvez maintenant créer :
[OK] Classes avec attributs et méthodes
[OK] Propriétés avec validation
[OK] Objets comparables et ordonnables
[OK] Objets avec opérateurs mathématiques
[OK] Objets itérables
[OK] Context managers personnalisés
[OK] Classes complètes et professionnelles


-> PROCHAINE ÉTAPE : PARTIE 2 - HÉRITAGE ET POLYMORPHISME

La Partie 2 couvrira :
- Héritage simple et multiple
- super() en profondeur
- Polymorphisme et duck typing
- Classes abstraites (ABC)
- Méthodes abstraites
- Mix-ins

Vous allez apprendre à :
- Créer hiérarchies de classes
- Réutiliser code efficacement
- Concevoir interfaces claires
- Utiliser polymorphisme


[GUIDE] FICHIER SUIVANT : poo_partie2.txt

Excellente progression ! Continuez ! [RAPIDE]
"""

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 2 : HÉRITAGE ET POLYMORPHISME
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 5 : Héritage Simple
# - Chapitre 6 : Héritage Multiple
# - Chapitre 7 : Polymorphisme
# - Chapitre 8 : Classes Abstraites
#
# [TEMPS] TEMPS : ~6-8 heures
# [DOCS] PRÉREQUIS : Partie 1 complétée
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 5 : HÉRITAGE SIMPLE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce que l'héritage et pourquoi l'utiliser
[OK] Créer une classe enfant
[OK] Utiliser super() correctement
[OK] Surcharger des méthodes (override)
[OK] Étendre des méthodes
[OK] isinstance() et issubclass()
"""


# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QUE L'HÉRITAGE ?
# ----------------------------------------------------------------------------

"""
DÉFINITION

HÉRITAGE = Mécanisme permettant à une classe (enfant) de 
            RÉUTILISER le code d'une autre classe (parent)

[IDEE] Analogie Biologique :
   Vous HÉRITEZ des caractéristiques de vos parents
   - Vous avez leurs yeux, leur nez
   - Mais vous êtes une personne unique
   - Vous pouvez avoir des caractéristiques propres


PROBLÈME SANS HÉRITAGE
"""

# [X] Code dupliqué (sans héritage)

class Chien:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
    
    def manger(self):
        print(f"{self.nom} mange")
    
    def dormir(self):
        print(f"{self.nom} dort")
    
    def aboyer(self):
        print(f"{self.nom} aboie : Wouf!")

class Chat:
    def __init__(self, nom, age):
        self.nom = nom        # <- Dupliqué
        self.age = age        # <- Dupliqué
    
    def manger(self):         # <- Dupliqué
        print(f"{self.nom} mange")
    
    def dormir(self):         # <- Dupliqué
        print(f"{self.nom} dort")
    
    def miauler(self):
        print(f"{self.nom} miaule : Miaou!")

"""
[X] Problèmes :
1. Code DUPLIQUÉ (__init__, manger, dormir)
2. Si bug dans manger() -> corriger dans TOUTES les classes
3. Ajouter attribut -> modifier TOUTES les classes
4. Difficile à maintenir


[OK] SOLUTION : HÉRITAGE
"""

class Animal:
    """Classe parent (classe de base)"""
    
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
    
    def manger(self):
        print(f"{self.nom} mange")
    
    def dormir(self):
        print(f"{self.nom} dort")

class Chien(Animal):  # <- Chien HÉRITE de Animal
    """Classe enfant"""
    
    def aboyer(self):
        print(f"{self.nom} aboie : Wouf!")

class Chat(Animal):   # <- Chat HÉRITE de Animal
    """Classe enfant"""
    
    def miauler(self):
        print(f"{self.nom} miaule : Miaou!")

# Test
rex = Chien("Rex", 5)
rex.manger()   # Hérité de Animal !
rex.dormir()   # Hérité de Animal !
rex.aboyer()   # Propre à Chien

felix = Chat("Felix", 3)
felix.manger()   # Hérité de Animal !
felix.dormir()   # Hérité de Animal !
felix.miauler()  # Propre à Chat

"""
[OK] Avantages :
1. Pas de duplication (DRY : Don't Repeat Yourself)
2. Code centralisé dans Animal
3. Modification unique pour tous
4. Hiérarchie claire et logique


[IDEE] TERMINOLOGIE

Animal  -> Classe PARENT (classe de base, superclasse)
Chien   -> Classe ENFANT (classe dérivée, sous-classe)

Chien HÉRITE de Animal
Chien EST UN Animal (relation "est-un")
"""


# ----------------------------------------------------------------------------
# [CONSTRUCTION] SYNTAXE DE L'HÉRITAGE
# ----------------------------------------------------------------------------

"""
SYNTAXE DE BASE
"""

class Parent:
    """Classe parent"""
    pass

class Enfant(Parent):  # <- Hérite de Parent
    """
    [IDEE] Syntaxe : class Enfant(Parent):
                         │       │
                         │       └─ Classe parent
                         └───────── Classe enfant
    """
    pass

"""
HÉRITAGE COMPLET
"""

class Vehicule:
    """Classe parent : Véhicule générique"""
    
    def __init__(self, marque, modele):
        self.marque = marque
        self.modele = modele
        self.vitesse = 0
    
    def accelerer(self):
        self.vitesse += 10
        print(f"{self.marque} {self.modele} accélère : {self.vitesse} km/h")
    
    def freiner(self):
        self.vitesse = max(0, self.vitesse - 10)
        print(f"{self.marque} {self.modele} freine : {self.vitesse} km/h")
    
    def afficher_info(self):
        print(f"{self.marque} {self.modele} - {self.vitesse} km/h")

class Voiture(Vehicule):
    """Classe enfant : Voiture spécifique"""
    
    def klaxonner(self):
        """Méthode spécifique aux voitures"""
        print(f"{self.marque} {self.modele} klaxonne : BIIIP!")

class Moto(Vehicule):
    """Classe enfant : Moto spécifique"""
    
    def faire_wheelie(self):
        """Méthode spécifique aux motos"""
        print(f"{self.marque} {self.modele} fait un wheelie !")

# Utilisation
ma_voiture = Voiture("Toyota", "Corolla")
ma_voiture.accelerer()      # Hérité de Vehicule
ma_voiture.klaxonner()      # Spécifique à Voiture

ma_moto = Moto("Honda", "CBR")
ma_moto.accelerer()         # Hérité de Vehicule
ma_moto.faire_wheelie()     # Spécifique à Moto

"""
[IDEE] QUE SE PASSE-T-IL ?

1. Création : ma_voiture = Voiture("Toyota", "Corolla")
   - Python cherche __init__ dans Voiture
   - Pas trouvé -> cherche dans parent (Vehicule)
   - Trouve et appelle Vehicule.__init__

2. Appel méthode : ma_voiture.accelerer()
   - Python cherche accelerer dans Voiture
   - Pas trouvé -> cherche dans parent
   - Trouve et appelle Vehicule.accelerer

3. Appel méthode : ma_voiture.klaxonner()
   - Python cherche klaxonner dans Voiture
   - Trouvé -> appelle Voiture.klaxonner


RECHERCHE DE MÉTHODE (Method Resolution Order - MRO)

Python cherche méthodes dans cet ordre :
1. Dans la classe de l'objet
2. Dans la classe parent
3. Dans le parent du parent
4. ... jusqu'à object (classe de base de tout)
"""

# Voir l'ordre de résolution
print(Voiture.__mro__)
# (<class '__main__.Voiture'>, <class '__main__.Vehicule'>, <class 'object'>)


# ----------------------------------------------------------------------------
# [OBJECTIF] SURCHARGE DE MÉTHODES (Override)
# ----------------------------------------------------------------------------

"""
SURCHARGE = Redéfinir une méthode du parent dans l'enfant
"""

class Animal:
    def __init__(self, nom):
        self.nom = nom
    
    def faire_son(self):
        print(f"{self.nom} fait un son générique")

class Chien(Animal):
    def faire_son(self):  # <- SURCHARGE
        """
        [IDEE] OVERRIDE : Remplacer méthode du parent
        
        Même nom, même signature
        Nouvelle implémentation
        """
        print(f"{self.nom} aboie : Wouf!")

class Chat(Animal):
    def faire_son(self):  # <- SURCHARGE
        print(f"{self.nom} miaule : Miaou!")

# Test
animaux = [
    Animal("Animal générique"),
    Chien("Rex"),
    Chat("Felix")
]

for animal in animaux:
    animal.faire_son()

# Sortie :
# Animal générique fait un son générique
# Rex aboie : Wouf!
# Felix miaule : Miaou!

"""
[IDEE] POLYMORPHISME en action !
Même appel (faire_son()), comportements différents


EXEMPLE COMPLET : FORMES GÉOMÉTRIQUES
"""

class Forme:
    """Classe parent abstraite"""
    
    def __init__(self, couleur):
        self.couleur = couleur
    
    def aire(self):
        """À surcharger dans les classes enfants"""
        raise NotImplementedError("Méthode à implémenter dans sous-classe")
    
    def perimetre(self):
        """À surcharger dans les classes enfants"""
        raise NotImplementedError("Méthode à implémenter dans sous-classe")
    
    def afficher(self):
        """Méthode commune"""
        print(f"{self.__class__.__name__} {self.couleur}")
        print(f"Aire: {self.aire():.2f}")
        print(f"Périmètre: {self.perimetre():.2f}")

class Rectangle(Forme):
    def __init__(self, couleur, largeur, hauteur):
        super().__init__(couleur)  # Appeler __init__ du parent
        self.largeur = largeur
        self.hauteur = hauteur
    
    def aire(self):  # <- SURCHARGE
        return self.largeur * self.hauteur
    
    def perimetre(self):  # <- SURCHARGE
        return 2 * (self.largeur + self.hauteur)

class Cercle(Forme):
    def __init__(self, couleur, rayon):
        super().__init__(couleur)
        self.rayon = rayon
    
    def aire(self):  # <- SURCHARGE
        import math
        return math.pi * self.rayon ** 2
    
    def perimetre(self):  # <- SURCHARGE
        import math
        return 2 * math.pi * self.rayon

# Test
formes = [
    Rectangle("rouge", 5, 3),
    Cercle("bleu", 4)
]

for forme in formes:
    forme.afficher()
    print()

# Sortie :
# Rectangle rouge
# Aire: 15.00
# Périmètre: 16.00
#
# Cercle bleu
# Aire: 50.27
# Périmètre: 25.13


# ----------------------------------------------------------------------------
# 🆙 UTILISER super()
# ----------------------------------------------------------------------------

"""
super() = Accéder à la classe parent

[IDEE] POURQUOI super() ?

Au lieu de :
    Parent.__init__(self, ...)  # [X] Nom en dur

Utilisez :
    super().__init__(...)       # [OK] Dynamique


EXEMPLE SANS super()
"""

class Employe:
    def __init__(self, nom, salaire):
        self.nom = nom
        self.salaire = salaire

class Manager(Employe):
    def __init__(self, nom, salaire, equipe):
        # [X] Appel explicite au parent (à éviter)
        Employe.__init__(self, nom, salaire)
        self.equipe = equipe

"""
EXEMPLE AVEC super()
"""

class Employe:
    def __init__(self, nom, salaire):
        self.nom = nom
        self.salaire = salaire
        print(f"Employé {nom} créé")

class Manager(Employe):
    def __init__(self, nom, salaire, equipe):
        # [OK] Appel via super()
        super().__init__(nom, salaire)
        self.equipe = equipe
        print(f"Manager avec équipe de {len(equipe)} personnes")

m = Manager("Alice", 80000, ["Bob", "Charlie"])
# Sortie :
# Employé Alice créé
# Manager avec équipe de 2 personnes

"""
[IDEE] AVANTAGES de super() :

1. Pas de nom de classe en dur
   Si vous renommez parent, super() marche toujours

2. Essentiel pour héritage multiple (Chapitre 6)

3. Plus pythonique


SUPER() DANS DIFFÉRENTES MÉTHODES
"""

class Vehicule:
    def __init__(self, marque):
        self.marque = marque
        self.vitesse = 0
    
    def accelerer(self):
        self.vitesse += 10
        print(f"Vitesse: {self.vitesse} km/h")
    
    def afficher_info(self):
        print(f"Véhicule {self.marque}")

class Voiture(Vehicule):
    def __init__(self, marque, nb_portes):
        super().__init__(marque)  # <- Dans __init__
        self.nb_portes = nb_portes
    
    def accelerer(self):
        # <- Appeler méthode parent PUIS ajouter comportement
        super().accelerer()  # Appeler Vehicule.accelerer()
        print("Vroum vroum!")
    
    def afficher_info(self):
        # <- Appeler méthode parent PUIS étendre
        super().afficher_info()
        print(f"{self.nb_portes} portes")

v = Voiture("Toyota", 4)
v.accelerer()
# Sortie :
# Vitesse: 10 km/h
# Vroum vroum!

v.afficher_info()
# Sortie :
# Véhicule Toyota
# 4 portes

"""
[CLE] PATTERN COURANT : Étendre méthode parent

def methode(self):
    super().methode()  # <- Comportement du parent
    # Code supplémentaire


EXEMPLE COMPLET : SYSTÈME DE FICHIERS
"""

class Fichier:
    """Classe parent : Fichier générique"""
    
    def __init__(self, nom, taille):
        self.nom = nom
        self.taille = taille  # en octets
    
    def afficher_info(self):
        print(f"Fichier: {self.nom}")
        print(f"Taille: {self.taille} octets")
    
    def ouvrir(self):
        print(f"Ouverture de {self.nom}")

class FichierTexte(Fichier):
    """Fichier texte avec encodage"""
    
    def __init__(self, nom, taille, encodage="utf-8"):
        super().__init__(nom, taille)
        self.encodage = encodage
    
    def afficher_info(self):
        super().afficher_info()  # Info de base
        print(f"Encodage: {self.encodage}")
    
    def ouvrir(self):
        super().ouvrir()
        print(f"Mode: Texte ({self.encodage})")

class FichierImage(Fichier):
    """Fichier image avec résolution"""
    
    def __init__(self, nom, taille, largeur, hauteur):
        super().__init__(nom, taille)
        self.largeur = largeur
        self.hauteur = hauteur
    
    def afficher_info(self):
        super().afficher_info()
        print(f"Résolution: {self.largeur}x{self.hauteur}")
    
    def ouvrir(self):
        super().ouvrir()
        print(f"Mode: Image ({self.largeur}x{self.hauteur} pixels)")

# Test
fichiers = [
    FichierTexte("doc.txt", 1024, "utf-8"),
    FichierImage("photo.jpg", 204800, 1920, 1080)
]

for fichier in fichiers:
    fichier.afficher_info()
    fichier.ouvrir()
    print()

# Sortie :
# Fichier: doc.txt
# Taille: 1024 octets
# Encodage: utf-8
# Ouverture de doc.txt
# Mode: Texte (utf-8)
#
# Fichier: photo.jpg
# Taille: 204800 octets
# Résolution: 1920x1080
# Ouverture de photo.jpg
# Mode: Image (1920x1080 pixels)


# ----------------------------------------------------------------------------
# [RECHERCHE] isinstance() ET issubclass()
# ----------------------------------------------------------------------------

"""
VÉRIFIER TYPE D'UN OBJET : isinstance()
"""

class Animal:
    pass

class Chien(Animal):
    pass

class Chat(Animal):
    pass

rex = Chien()
felix = Chat()

# isinstance(objet, classe)
print(isinstance(rex, Chien))    # True
print(isinstance(rex, Animal))   # True <- Aussi parent !
print(isinstance(rex, Chat))     # False

print(isinstance(felix, Chat))   # True
print(isinstance(felix, Animal)) # True
print(isinstance(felix, Chien))  # False

# Plusieurs classes possibles
print(isinstance(rex, (Chien, Chat)))  # True (Chien OU Chat)

"""
[IDEE] isinstance() avec héritage :

isinstance(obj, Classe) retourne True si :
- obj est instance de Classe
- obj est instance d'une SOUS-CLASSE de Classe


VÉRIFIER HÉRITAGE : issubclass()
"""

# issubclass(classe_enfant, classe_parent)
print(issubclass(Chien, Animal))   # True
print(issubclass(Chat, Animal))    # True
print(issubclass(Chien, Chat))     # False
print(issubclass(Animal, object))  # True (tout hérite de object)

"""
UTILISATION PRATIQUE
"""

def traiter_animal(animal):
    """Fonction qui traite différents types d'animaux"""
    
    if not isinstance(animal, Animal):
        raise TypeError("Doit être un Animal")
    
    if isinstance(animal, Chien):
        animal.aboyer()
    elif isinstance(animal, Chat):
        animal.miauler()
    else:
        print("Animal générique")

"""
[ATTENTION] DUCK TYPING : Préférer ça en Python

Au lieu de vérifier type, vérifier capacités :
"""

def traiter_animal_duck_typing(animal):
    """Version "duck typing" (plus pythonique)"""
    
    # Si ça aboie comme un chien...
    if hasattr(animal, 'aboyer'):
        animal.aboyer()
    # Si ça miaule comme un chat...
    elif hasattr(animal, 'miauler'):
        animal.miauler()
    else:
        print("Animal sans son spécifique")

"""
[IDEE] "If it walks like a duck and quacks like a duck, it's a duck"

En Python, on se fiche du TYPE exact,
on vérifie juste qu'il a les MÉTHODES nécessaires.
"""


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 3 : HIÉRARCHIE DE COMPTES BANCAIRES
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer hiérarchie de comptes bancaires

SPÉCIFICATIONS :

1. Classe CompteBancaire (parent) :
   - Attributs : numero, titulaire, solde
   - Méthodes : deposer(), retirer(), afficher_solde()

2. Classe CompteCourant (enfant) :
   - Attribut supplémentaire : decouvert_autorise
   - Surcharge retirer() : Permettre découvert

3. Classe CompteEpargne (enfant) :
   - Attribut supplémentaire : taux_interet
   - Méthode : calculer_interets()


SOLUTION :
"""

class CompteBancaire:
    """Classe parent : Compte bancaire générique"""
    
    def __init__(self, numero, titulaire, solde_initial=0):
        self.numero = numero
        self.titulaire = titulaire
        self.solde = solde_initial
    
    def deposer(self, montant):
        """Déposer de l'argent"""
        if montant > 0:
            self.solde += montant
            print(f"Dépôt de {montant}€. Nouveau solde : {self.solde}€")
        else:
            print("Montant invalide")
    
    def retirer(self, montant):
        """Retirer de l'argent"""
        if montant > 0 and self.solde >= montant:
            self.solde -= montant
            print(f"Retrait de {montant}€. Nouveau solde : {self.solde}€")
        else:
            print("Retrait impossible (solde insuffisant)")
    
    def afficher_solde(self):
        """Afficher le solde"""
        print(f"Compte {self.numero} ({self.titulaire}) : {self.solde}€")
    
    def __str__(self):
        return f"Compte {self.numero} - {self.titulaire}: {self.solde}€"

class CompteCourant(CompteBancaire):
    """Compte courant avec découvert autorisé"""
    
    def __init__(self, numero, titulaire, solde_initial=0, decouvert=500):
        super().__init__(numero, titulaire, solde_initial)
        self.decouvert_autorise = decouvert
    
    def retirer(self, montant):
        """
        Retrait avec découvert autorisé
        SURCHARGE de la méthode parent
        """
        limite = self.solde + self.decouvert_autorise
        
        if montant > 0 and montant <= limite:
            self.solde -= montant
            print(f"Retrait de {montant}€. Nouveau solde : {self.solde}€")
            
            if self.solde < 0:
                print(f"[ATTENTION] Découvert : {abs(self.solde)}€ (max {self.decouvert_autorise}€)")
        else:
            print(f"Retrait impossible (limite de découvert dépassée)")
    
    def afficher_solde(self):
        """Afficher solde avec info découvert"""
        super().afficher_solde()
        if self.solde < 0:
            print(f"Découvert : {abs(self.solde)}€ / {self.decouvert_autorise}€")
        else:
            print(f"Découvert autorisé : {self.decouvert_autorise}€")

class CompteEpargne(CompteBancaire):
    """Compte épargne avec intérêts"""
    
    def __init__(self, numero, titulaire, solde_initial=0, taux_interet=2.5):
        super().__init__(numero, titulaire, solde_initial)
        self.taux_interet = taux_interet  # En pourcentage
    
    def retirer(self, montant):
        """
        Retrait limité (pas de découvert)
        On garde la version du parent, mais on peut ajouter conditions
        """
        if montant > 100:
            print("[ATTENTION] Retrait important d'un compte épargne")
        
        super().retirer(montant)
    
    def calculer_interets(self):
        """Calculer et ajouter les intérêts"""
        interets = self.solde * (self.taux_interet / 100)
        self.solde += interets
        print(f"Intérêts calculés : {interets:.2f}€ (taux : {self.taux_interet}%)")
        print(f"Nouveau solde : {self.solde:.2f}€")
    
    def afficher_solde(self):
        """Afficher solde avec taux d'intérêt"""
        super().afficher_solde()
        print(f"Taux d'intérêt : {self.taux_interet}%")

# Test complet
print("=== COMPTE BANCAIRE CLASSIQUE ===")
compte1 = CompteBancaire("001", "Alice", 1000)
compte1.afficher_solde()
compte1.retirer(1500)  # Impossible
compte1.retirer(300)   # OK
print()

print("=== COMPTE COURANT (avec découvert) ===")
compte2 = CompteCourant("002", "Bob", 500, decouvert=300)
compte2.afficher_solde()
compte2.retirer(600)   # OK (découvert)
compte2.afficher_solde()
compte2.retirer(300)   # Impossible (dépasse découvert)
print()

print("=== COMPTE ÉPARGNE (avec intérêts) ===")
compte3 = CompteEpargne("003", "Charlie", 10000, taux_interet=3.0)
compte3.afficher_solde()
compte3.calculer_interets()
compte3.retirer(500)
print()

# Polymorphisme : traiter tous les comptes de la même façon
print("=== LISTE DE COMPTES ===")
comptes = [compte1, compte2, compte3]

for compte in comptes:
    print(compte)  # Appelle __str__

# Sortie complète :
# === COMPTE BANCAIRE CLASSIQUE ===
# Compte 001 (Alice) : 1000€
# Retrait impossible (solde insuffisant)
# Retrait de 300€. Nouveau solde : 700€
#
# === COMPTE COURANT (avec découvert) ===
# Compte 002 (Bob) : 500€
# Découvert autorisé : 300€
# Retrait de 600€. Nouveau solde : -100€
# [ATTENTION] Découvert : 100€ (max 300€)
# Compte 002 (Bob) : -100€
# Découvert : 100€ / 300€
# Retrait impossible (limite de découvert dépassée)
#
# === COMPTE ÉPARGNE (avec intérêts) ===
# Compte 003 (Charlie) : 10000€
# Taux d'intérêt : 3.0%
# Intérêts calculés : 300.00€ (taux : 3.0%)
# Nouveau solde : 10300.00€
# [ATTENTION] Retrait important d'un compte épargne
# Retrait de 500€. Nouveau solde : 9800.0€
#
# === LISTE DE COMPTES ===
# Compte 001 - Alice: 700€
# Compte 002 - Bob: -100€
# Compte 003 - Charlie: 9800.0€


# Ce fichier continue avec Chapitres 6, 7, 8...
# ============================================================================

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 2 (SUITE) : HÉRITAGE MULTIPLE ET POLYMORPHISME
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 6 : Héritage Multiple
# - Chapitre 7 : Polymorphisme
# - Chapitre 8 : Classes Abstraites
#
# [TEMPS] TEMPS : ~4-5 heures
# [DOCS] PRÉREQUIS : Chapitre 5 complété
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 6 : HÉRITAGE MULTIPLE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce que l'héritage multiple
[OK] Créer une classe héritant de plusieurs parents
[OK] Comprendre le MRO (Method Resolution Order)
[OK] Résoudre le problème du diamant
[OK] Utiliser les Mix-ins
[OK] Quand éviter l'héritage multiple
"""


# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QUE L'HÉRITAGE MULTIPLE ?
# ----------------------------------------------------------------------------

"""
DÉFINITION

HÉRITAGE MULTIPLE = Une classe peut hériter de PLUSIEURS parents

[IDEE] Analogie :
   Vous héritez de caractéristiques de DEUX parents
   - Yeux de votre mère
   - Nez de votre père
   - Taille de votre grand-père


SYNTAXE
"""

class Parent1:
    pass

class Parent2:
    pass

class Enfant(Parent1, Parent2):  # <- Hérite de Parent1 ET Parent2
    """
    [IDEE] Syntaxe : class Enfant(Parent1, Parent2, ...):
                         │       │        │
                         │       │        └─ Parent 2
                         │       └────────── Parent 1
                         └────────────────── Classe enfant
    """
    pass

"""
EXEMPLE SIMPLE
"""

class Nageur:
    def nager(self):
        print("Je nage")

class Volant:
    def voler(self):
        print("Je vole")

class Canard(Nageur, Volant):  # <- Héritage multiple
    """Le canard peut nager ET voler"""
    def crier(self):
        print("Coin coin!")

# Test
donald = Canard()
donald.nager()   # Hérité de Nageur
donald.voler()   # Hérité de Volant
donald.crier()   # Propre à Canard

"""
[OK] Canard a maintenant :
- Les méthodes de Nageur
- Les méthodes de Volant
- Ses propres méthodes


EXEMPLE PRATIQUE : APPAREIL MULTIFONCTION
"""

class Imprimante:
    def __init__(self):
        self.papier = 100
    
    def imprimer(self, document):
        if self.papier > 0:
            self.papier -= 1
            print(f"Impression de '{document}' (papier restant: {self.papier})")
        else:
            print("Plus de papier!")

class Scanner:
    def __init__(self):
        self.scans = []
    
    def scanner(self, document):
        self.scans.append(document)
        print(f"Scan de '{document}' (total scans: {len(self.scans)})")

class Fax:
    def __init__(self):
        self.fax_envoyes = 0
    
    def envoyer_fax(self, numero, document):
        self.fax_envoyes += 1
        print(f"Fax envoyé à {numero}: '{document}' (total: {self.fax_envoyes})")

class AppareilMultifonction(Imprimante, Scanner, Fax):
    """Appareil combinant impression, scan et fax"""
    
    def __init__(self):
        # [ATTENTION] Problème : Plusieurs __init__ !
        # Solution : Appeler tous les parents
        Imprimante.__init__(self)
        Scanner.__init__(self)
        Fax.__init__(self)
    
    def statut(self):
        print(f"Papier: {self.papier}")
        print(f"Scans: {len(self.scans)}")
        print(f"Fax envoyés: {self.fax_envoyes}")

# Test
appareil = AppareilMultifonction()
appareil.imprimer("Rapport")
appareil.scanner("Contrat")
appareil.envoyer_fax("0123456789", "Facture")
appareil.statut()

"""
[ATTENTION] PROBLÈME : CONFLIT DE NOMS

Que se passe-t-il si plusieurs parents ont une méthode avec le même nom ?
"""


# ----------------------------------------------------------------------------
# [MELANGE] MRO : METHOD RESOLUTION ORDER
# ----------------------------------------------------------------------------

"""
MRO = Ordre dans lequel Python cherche les méthodes

[IDEE] PROBLÈME
"""

class A:
    def methode(self):
        print("Méthode de A")

class B:
    def methode(self):
        print("Méthode de B")

class C(A, B):  # <- Quelle méthode sera appelée ?
    pass

c = C()
c.methode()  # "Méthode de A"

"""
[REFLEXION] POURQUOI "Méthode de A" ?

Python utilise le MRO (Method Resolution Order) :
1. Cherche dans C
2. Pas trouvé -> Cherche dans A (premier parent listé)
3. Trouvé -> Utilise A.methode()

Si pas trouvé dans A -> cherche dans B
Si pas trouvé dans B -> cherche dans leurs parents
...jusqu'à object


VOIR LE MRO
"""

print(C.__mro__)
# (<class 'C'>, <class 'A'>, <class 'B'>, <class 'object'>)

# Ou de manière plus lisible
print(C.mro())
# [<class 'C'>, <class 'A'>, <class 'B'>, <class 'object'>]

"""
[IDEE] ORDRE DE RÉSOLUTION

C -> A -> B -> object

Python cherche dans cet ordre (de gauche à droite)


ALGORITHME C3 LINEARIZATION

Python 3 utilise C3 Linearization pour calculer le MRO
Règles simplifiées :
1. Enfant avant parents
2. Parents dans l'ordre de déclaration
3. Cohérence de l'ordre dans toute la hiérarchie


EXEMPLE COMPLEXE
"""

class A:
    def methode(self):
        print("A")

class B(A):
    def methode(self):
        print("B")

class C(A):
    def methode(self):
        print("C")

class D(B, C):
    pass

print(D.mro())
# [D, B, C, A, object]

d = D()
d.methode()  # "B" (B est avant C dans le MRO)

"""
DIAGRAMME

       A
      / \
     B   C
      \ /
       D

MRO de D : D -> B -> C -> A -> object
"""


# ----------------------------------------------------------------------------
# [GEM_STONE] PROBLÈME DU DIAMANT
# ----------------------------------------------------------------------------

"""
PROBLÈME DU DIAMANT

Situation où une classe hérite deux fois de la même classe parent

       A
      / \
     B   C
      \ /
       D

D hérite de B et C, qui héritent tous deux de A
-> D hérite deux fois de A (indirectement)


EXEMPLE CONCRET
"""

class Vehicule:
    def __init__(self, marque):
        print(f"Vehicule.__init__({marque})")
        self.marque = marque

class Terrestre(Vehicule):
    def __init__(self, marque, roues):
        print(f"Terrestre.__init__({marque}, {roues})")
        super().__init__(marque)
        self.roues = roues

class Aquatique(Vehicule):
    def __init__(self, marque, capacite):
        print(f"Aquatique.__init__({marque}, {capacite})")
        super().__init__(marque)
        self.capacite = capacite

class Amphibie(Terrestre, Aquatique):
    def __init__(self, marque, roues, capacite):
        print(f"Amphibie.__init__({marque}, {roues}, {capacite})")
        super().__init__(marque, roues)
        # [ATTENTION] Problème : Comment initialiser Aquatique aussi ?

# Test
# vehicule = Amphibie("AmphibieCar", 4, 1000)

"""
[X] PROBLÈMES :

1. Vehicule.__init__ pourrait être appelé plusieurs fois
2. Comment initialiser Terrestre ET Aquatique ?
3. Ordre d'appel des __init__ ?


[OK] SOLUTION : super() + MRO

super() suit le MRO, donc appelle chaque classe UNE SEULE FOIS
"""

class Vehicule:
    def __init__(self, marque, **kwargs):
        print(f"Vehicule.__init__({marque})")
        self.marque = marque
        super().__init__(**kwargs)  # <- Passer kwargs restants

class Terrestre(Vehicule):
    def __init__(self, roues, **kwargs):
        print(f"Terrestre.__init__({roues})")
        self.roues = roues
        super().__init__(**kwargs)  # <- Passer kwargs restants

class Aquatique(Vehicule):
    def __init__(self, capacite, **kwargs):
        print(f"Aquatique.__init__({capacite})")
        self.capacite = capacite
        super().__init__(**kwargs)  # <- Passer kwargs restants

class Amphibie(Terrestre, Aquatique):
    def __init__(self, marque, roues, capacite):
        print(f"Amphibie.__init__({marque}, {roues}, {capacite})")
        super().__init__(
            marque=marque,
            roues=roues,
            capacite=capacite
        )

# Test
print("MRO:", Amphibie.mro())
print("\nCréation véhicule amphibie :")
vehicule = Amphibie("AmphibieCar", 4, 1000)

# Sortie :
# MRO: [Amphibie, Terrestre, Aquatique, Vehicule, object]
#
# Création véhicule amphibie :
# Amphibie.__init__(AmphibieCar, 4, 1000)
# Terrestre.__init__(4)
# Aquatique.__init__(1000)
# Vehicule.__init__(AmphibieCar)

print(f"\nMarque: {vehicule.marque}")
print(f"Roues: {vehicule.roues}")
print(f"Capacité: {vehicule.capacite}")

"""
[IDEE] SOLUTION EXPLIQUÉE

1. **kwargs permet de passer arguments aux parents suivants
2. super() suit le MRO -> appelle dans l'ordre
3. Chaque __init__ appelé UNE SEULE FOIS

MRO : Amphibie -> Terrestre -> Aquatique -> Vehicule

Amphibie.super() -> Terrestre
Terrestre.super() -> Aquatique (pas Vehicule!)
Aquatique.super() -> Vehicule
Vehicule.super() -> object
"""


# ----------------------------------------------------------------------------
# [DESIGN] MIX-INS
# ----------------------------------------------------------------------------

"""
MIX-IN = Classe conçue pour ajouter fonctionnalité spécifique

[IDEE] Caractéristiques d'un Mix-in :
- Pas utilisé seul (pas instancié directement)
- Ajoute UNE fonctionnalité précise
- Pas de __init__ (ou __init__ qui appelle super())
- Nom souvent en "-Mixin"


EXEMPLE : SYSTÈME DE LOG
"""

class LogMixin:
    """Mix-in pour ajouter capacité de logging"""
    
    def log(self, message):
        """Affiche message avec nom de la classe"""
        print(f"[{self.__class__.__name__}] {message}")

class SauvegardeMixin:
    """Mix-in pour ajouter sauvegarde"""
    
    def sauvegarder(self):
        """Sauvegarde l'état de l'objet"""
        print(f"Sauvegarde de {self.__class__.__name__}")
        # Code de sauvegarde réel ici

class Utilisateur(LogMixin, SauvegardeMixin):
    """Utilisateur avec log et sauvegarde"""
    
    def __init__(self, nom):
        self.nom = nom
    
    def faire_action(self):
        self.log(f"{self.nom} fait une action")
        # ... action ...
        self.sauvegarder()

# Test
user = Utilisateur("Alice")
user.faire_action()
# [Utilisateur] Alice fait une action
# Sauvegarde de Utilisateur

"""
EXEMPLE COMPLET : SYSTÈME DE COMPARAISON
"""

class ComparaisonMixin:
    """
    Mix-in ajoutant opérateurs de comparaison
    Nécessite que la classe définisse _cle_comparaison()
    """
    
    def _cle_comparaison(self):
        """À définir dans la classe utilisatrice"""
        raise NotImplementedError("Définir _cle_comparaison()")
    
    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return self._cle_comparaison() == other._cle_comparaison()
    
    def __lt__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return self._cle_comparaison() < other._cle_comparaison()
    
    def __le__(self, other):
        return self == other or self < other
    
    def __gt__(self, other):
        return not self <= other
    
    def __ge__(self, other):
        return not self < other

class Etudiant(ComparaisonMixin):
    """Étudiant comparable par moyenne"""
    
    def __init__(self, nom, moyenne):
        self.nom = nom
        self.moyenne = moyenne
    
    def _cle_comparaison(self):
        """Comparer par moyenne"""
        return self.moyenne
    
    def __str__(self):
        return f"{self.nom} (moyenne: {self.moyenne})"

class Employe(ComparaisonMixin):
    """Employé comparable par salaire"""
    
    def __init__(self, nom, salaire):
        self.nom = nom
        self.salaire = salaire
    
    def _cle_comparaison(self):
        """Comparer par salaire"""
        return self.salaire
    
    def __str__(self):
        return f"{self.nom} (salaire: {self.salaire}€)"

# Test Étudiant
etudiants = [
    Etudiant("Alice", 15),
    Etudiant("Bob", 12),
    Etudiant("Charlie", 18)
]

etudiants_tries = sorted(etudiants)
print("Étudiants triés par moyenne :")
for e in etudiants_tries:
    print(e)

# Test Employé
employes = [
    Employe("David", 35000),
    Employe("Eve", 50000),
    Employe("Frank", 42000)
]

employes_tries = sorted(employes)
print("\nEmployés triés par salaire :")
for emp in employes_tries:
    print(emp)

"""
[IDEE] AVANTAGES DES MIX-INS

1. RÉUTILISABILITÉ
   Même fonctionnalité pour plusieurs classes

2. SÉPARATION DES PRÉOCCUPATIONS
   Chaque mix-in = une fonctionnalité

3. COMPOSITION
   Combiner plusieurs mix-ins

4. ÉVITE DUPLICATION
   Code écrit une fois


EXEMPLE : PLUSIEURS MIX-INS
"""

class JsonMixin:
    """Mix-in pour convertir en JSON"""
    
    def to_json(self):
        import json
        # Convertir attributs publics en dict
        data = {k: v for k, v in self.__dict__.items() if not k.startswith('_')}
        return json.dumps(data, indent=2)

class ReprMixin:
    """Mix-in pour __repr__ automatique"""
    
    def __repr__(self):
        attrs = ', '.join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{self.__class__.__name__}({attrs})"

class Produit(JsonMixin, ReprMixin):
    """Produit avec JSON et repr"""
    
    def __init__(self, nom, prix, stock):
        self.nom = nom
        self.prix = prix
        self.stock = stock

# Test
p = Produit("Laptop", 999.99, 10)

print(repr(p))
# Produit(nom='Laptop', prix=999.99, stock=10)

print(p.to_json())
# {
#   "nom": "Laptop",
#   "prix": 999.99,
#   "stock": 10
# }


# ----------------------------------------------------------------------------
# [ATTENTION] QUAND ÉVITER L'HÉRITAGE MULTIPLE
# ----------------------------------------------------------------------------

"""
[X] CAS OÙ L'HÉRITAGE MULTIPLE POSE PROBLÈME

1. HIÉRARCHIES COMPLEXES
   Plus de 2-3 parents -> difficile à comprendre

2. CONFLITS DE NOMS FRÉQUENTS
   Beaucoup de méthodes en conflit

3. ALTERNATIVES PLUS SIMPLES
   Composition parfois meilleure


[OK] PRÉFÉRER COMPOSITION À HÉRITAGE
"""

# [X] Héritage multiple complexe
class MoteurElectrique:
    def demarrer(self):
        print("Moteur électrique démarre")

class MoteurThermique:
    def demarrer(self):
        print("Moteur thermique démarre")

class VoitureHybride(MoteurElectrique, MoteurThermique):
    pass  # Quel demarrer() sera appelé ?

# [OK] Composition (meilleur)
class VoitureHybride:
    def __init__(self):
        self.moteur_electrique = MoteurElectrique()
        self.moteur_thermique = MoteurThermique()
    
    def demarrer_electrique(self):
        self.moteur_electrique.demarrer()
    
    def demarrer_thermique(self):
        self.moteur_thermique.demarrer()

"""
[CLE] RÈGLE GÉNÉRALE

Utilisez héritage multiple pour :
[OK] Mix-ins (ajouter fonctionnalités)
[OK] Interfaces multiples (classes abstraites)
[OK] Cas simples et clairs

Évitez pour :
[X] Hiérarchies complexes
[X] Logique métier principale
[X] Quand composition est plus claire
"""


# ============================================================================
# [GUIDE] CHAPITRE 7 : POLYMORPHISME
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce que le polymorphisme
[OK] Polymorphisme par héritage
[OK] Duck typing en Python
[OK] Interfaces implicites
[OK] Polymorphisme avec méthodes abstraites
"""


# ----------------------------------------------------------------------------
# [SCENARIO] QU'EST-CE QUE LE POLYMORPHISME ?
# ----------------------------------------------------------------------------

"""
DÉFINITION

POLYMORPHISME = "Plusieurs formes"
                Capacité d'utiliser objets de classes différentes
                de la MÊME MANIÈRE

[IDEE] Analogie :
   Télécommande universelle
   - Même bouton "Play"
   - Fonctionne avec TV, DVD, Lecteur Blu-ray
   - Chacun réagit différemment
   - Vous utilisez toujours "Play"


EXEMPLE SIMPLE
"""

class Chien:
    def faire_son(self):
        return "Wouf!"

class Chat:
    def faire_son(self):
        return "Miaou!"

class Vache:
    def faire_son(self):
        return "Meuh!"

def faire_crier_animal(animal):
    """
    [IDEE] POLYMORPHISME
    
    Peu importe le type exact de l'animal,
    tant qu'il a une méthode faire_son()
    """
    print(animal.faire_son())

# Test
animaux = [Chien(), Chat(), Vache()]

for animal in animaux:
    faire_crier_animal(animal)

# Sortie :
# Wouf!
# Miaou!
# Meuh!

"""
[OK] MÊME APPEL (faire_son())
[OK] COMPORTEMENTS DIFFÉRENTS
[OK] Code qui utilise ne se soucie pas du type exact
"""


# ----------------------------------------------------------------------------
# [CONSTRUCTION] POLYMORPHISME PAR HÉRITAGE
# ----------------------------------------------------------------------------

"""
POLYMORPHISME AVEC CLASSE PARENT COMMUNE
"""

class Forme:
    """Classe parent"""
    
    def aire(self):
        """À surcharger"""
        raise NotImplementedError("Méthode à implémenter")
    
    def perimetre(self):
        """À surcharger"""
        raise NotImplementedError("Méthode à implémenter")

class Rectangle(Forme):
    def __init__(self, largeur, hauteur):
        self.largeur = largeur
        self.hauteur = hauteur
    
    def aire(self):
        return self.largeur * self.hauteur
    
    def perimetre(self):
        return 2 * (self.largeur + self.hauteur)

class Cercle(Forme):
    def __init__(self, rayon):
        self.rayon = rayon
    
    def aire(self):
        import math
        return math.pi * self.rayon ** 2
    
    def perimetre(self):
        import math
        return 2 * math.pi * self.rayon

class Triangle(Forme):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c
    
    def aire(self):
        # Formule de Héron
        s = self.perimetre() / 2
        import math
        return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
    
    def perimetre(self):
        return self.a + self.b + self.c

def afficher_infos_forme(forme):
    """
    [IDEE] POLYMORPHISME
    
    Fonction générique qui fonctionne avec TOUTE forme
    """
    print(f"{forme.__class__.__name__}:")
    print(f"  Aire: {forme.aire():.2f}")
    print(f"  Périmètre: {forme.perimetre():.2f}")

# Test
formes = [
    Rectangle(5, 3),
    Cercle(4),
    Triangle(3, 4, 5)
]

for forme in formes:
    afficher_infos_forme(forme)
    print()

# Sortie :
# Rectangle:
#   Aire: 15.00
#   Périmètre: 16.00
#
# Cercle:
#   Aire: 50.27
#   Périmètre: 25.13
#
# Triangle:
#   Aire: 6.00
#   Périmètre: 12.00

"""
[IDEE] AVANTAGES

1. CODE GÉNÉRIQUE
   afficher_infos_forme() fonctionne avec toute forme

2. EXTENSIBLE
   Ajouter Triangle -> pas besoin de modifier afficher_infos_forme()

3. LISIBLE
   Interface claire (aire, perimetre)
"""


# ----------------------------------------------------------------------------
# [DUCK] DUCK TYPING
# ----------------------------------------------------------------------------

"""
DUCK TYPING = "Si ça marche comme un canard, c'est un canard"

[IDEE] En Python, on se fiche du TYPE
   On vérifie juste qu'il a les MÉTHODES nécessaires

Citation célèbre :
"If it walks like a duck and quacks like a duck, it's a duck"


EXEMPLE SANS HÉRITAGE
"""

# Pas de classe parent commune !

class Fichier:
    def __init__(self, nom):
        self.nom = nom
    
    def lire(self):
        return f"Contenu de {self.nom}"

class BaseDeDonnees:
    def __init__(self, nom):
        self.nom = nom
    
    def lire(self):
        return f"Données de {self.nom}"

class API:
    def __init__(self, url):
        self.url = url
    
    def lire(self):
        return f"Réponse de {self.url}"

def traiter_source(source):
    """
    [IDEE] DUCK TYPING
    
    Peu importe le type de source,
    tant qu'elle a une méthode lire()
    """
    contenu = source.lire()
    print(f"Traitement : {contenu}")

# Test
sources = [
    Fichier("data.txt"),
    BaseDeDonnees("users"),
    API("https://api.exemple.com")
]

for source in sources:
    traiter_source(source)

# Sortie :
# Traitement : Contenu de data.txt
# Traitement : Données de users
# Traitement : Réponse de https://api.exemple.com

"""
[OK] Aucun héritage commun
[OK] Mais toutes ont lire()
[OK] Duck typing fonctionne !


VÉRIFIER CAPACITÉS AVEC hasattr()
"""

def traiter_source_secure(source):
    """Version avec vérification"""
    
    if not hasattr(source, 'lire'):
        raise TypeError("L'objet doit avoir une méthode lire()")
    
    contenu = source.lire()
    print(f"Traitement : {contenu}")

"""
DUCK TYPING AVEC PROTOCOLES PYTHON
"""

class MaListe:
    """Liste personnalisée qui fonctionne avec len(), [], etc."""
    
    def __init__(self):
        self.items = []
    
    def __len__(self):
        """len(obj) fonctionne"""
        return len(self.items)
    
    def __getitem__(self, index):
        """obj[index] fonctionne"""
        return self.items[index]
    
    def __iter__(self):
        """for item in obj fonctionne"""
        return iter(self.items)
    
    def append(self, item):
        """Comme une vraie liste"""
        self.items.append(item)

# Test
ma_liste = MaListe()
ma_liste.append("A")
ma_liste.append("B")
ma_liste.append("C")

# Fonctionne comme une liste !
print(len(ma_liste))  # 3
print(ma_liste[1])    # B

for item in ma_liste:
    print(item)       # A, B, C

"""
[IDEE] PROTOCOLES PYTHON

Protocole = Ensemble de méthodes spéciales qui définissent un comportement

Protocole "Séquence" :
- __len__, __getitem__

Protocole "Itérable" :
- __iter__, __next__

Protocole "Context Manager" :
- __enter__, __exit__

Si votre objet a ces méthodes -> il "est" ce type (duck typing)
"""


# ----------------------------------------------------------------------------
# [LISTE] INTERFACES IMPLICITES
# ----------------------------------------------------------------------------

"""
INTERFACE = Contrat définissant méthodes à implémenter

Python n'a pas d'interfaces formelles (comme Java)
Mais on peut les simuler


PATTERN : CLASSE DE BASE AVEC NotImplementedError
"""

class Paiement:
    """Interface pour systèmes de paiement"""
    
    def autoriser(self, montant):
        raise NotImplementedError("Méthode à implémenter")
    
    def capturer(self, montant):
        raise NotImplementedError("Méthode à implémenter")
    
    def rembourser(self, montant):
        raise NotImplementedError("Méthode à implémenter")

class PaiementCarte(Paiement):
    def __init__(self, numero_carte):
        self.numero_carte = numero_carte
    
    def autoriser(self, montant):
        print(f"Autorisation carte {self.numero_carte}: {montant}€")
        return True
    
    def capturer(self, montant):
        print(f"Capture carte {self.numero_carte}: {montant}€")
        return True
    
    def rembourser(self, montant):
        print(f"Remboursement carte {self.numero_carte}: {montant}€")
        return True

class PaiementPayPal(Paiement):
    def __init__(self, email):
        self.email = email
    
    def autoriser(self, montant):
        print(f"Autorisation PayPal {self.email}: {montant}€")
        return True
    
    def capturer(self, montant):
        print(f"Capture PayPal {self.email}: {montant}€")
        return True
    
    def rembourser(self, montant):
        print(f"Remboursement PayPal {self.email}: {montant}€")
        return True

def processus_paiement(paiement: Paiement, montant: float):
    """
    [IDEE] POLYMORPHISME VIA INTERFACE
    
    Fonctionne avec tout objet respectant l'interface Paiement
    """
    if paiement.autoriser(montant):
        if paiement.capturer(montant):
            print("Paiement réussi!")
        else:
            print("Échec capture")
    else:
        print("Échec autorisation")

# Test
paiements = [
    PaiementCarte("1234-5678-9012-3456"),
    PaiementPayPal("alice@example.com")
]

for p in paiements:
    processus_paiement(p, 99.99)
    print()

"""
[ATTENTION] PROBLÈME : Oubli d'implémenter méthode
"""

class PaiementCheque(Paiement):
    def autoriser(self, montant):
        return True
    
    def capturer(self, montant):
        return True
    
    # [X] Oublié rembourser() !

cheque = PaiementCheque()
# cheque.rembourser(50)  # NotImplementedError

"""
Pour forcer l'implémentation -> Classes Abstraites (Chapitre 8)
"""


# Ce fichier continue avec le Chapitre 8...
# ============================================================================

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 2 (FIN) : CLASSES ABSTRAITES ET RÉCAPITULATIF
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 8 : Classes Abstraites (ABC) - Complet
# - Exercices Pratiques
# - Récapitulatif Partie 2
#
# [TEMPS] TEMPS : ~2-3 heures
# [DOCS] PRÉREQUIS : Chapitres 5, 6 et 7 complétés
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 8 : CLASSES ABSTRAITES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce qu'une classe abstraite
[OK] Utiliser le module abc
[OK] Créer méthodes abstraites avec @abstractmethod
[OK] Propriétés abstraites
[OK] Forcer implémentation dans sous-classes
[OK] Interfaces avec ABC
"""


# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QU'UNE CLASSE ABSTRAITE ?
# ----------------------------------------------------------------------------

"""
DÉFINITION

CLASSE ABSTRAITE = Classe qui NE PEUT PAS être instanciée directement
                   Sert de MODÈLE pour les sous-classes

[IDEE] Analogie :
   "Véhicule" est un concept abstrait
   Vous ne pouvez pas acheter un "véhicule"
   Vous achetez une voiture, une moto, un vélo (concrets)


PROBLÈME SANS CLASSES ABSTRAITES
"""

class Forme:
    """Classe de base pour formes géométriques"""
    
    def aire(self):
        raise NotImplementedError("À implémenter dans sous-classe")
    
    def perimetre(self):
        raise NotImplementedError("À implémenter dans sous-classe")

# [X] On peut créer une Forme directement !
forme = Forme()

# [X] Erreur seulement à l'exécution
# forme.aire()  # NotImplementedError

"""
[X] Problèmes :
1. Forme() ne devrait pas être possible
2. Erreur seulement à l'exécution (pas à la création)
3. Pas de vérification que sous-classes implémentent méthodes


[OK] SOLUTION : CLASSES ABSTRAITES (ABC)
"""

from abc import ABC, abstractmethod

class Forme(ABC):  # <- Hérite de ABC
    """Classe abstraite pour formes géométriques"""
    
    @abstractmethod  # <- Méthode abstraite
    def aire(self):
        """Doit être implémentée dans sous-classes"""
        pass
    
    @abstractmethod
    def perimetre(self):
        """Doit être implémentée dans sous-classes"""
        pass

# [X] Impossible de créer une instance !
# forme = Forme()  # TypeError: Can't instantiate abstract class

# [OK] Mais peut créer sous-classes
class Rectangle(Forme):
    def __init__(self, largeur, hauteur):
        self.largeur = largeur
        self.hauteur = hauteur
    
    def aire(self):  # <- Implémentation obligatoire
        return self.largeur * self.hauteur
    
    def perimetre(self):  # <- Implémentation obligatoire
        return 2 * (self.largeur + self.hauteur)

# [OK] Maintenant on peut créer Rectangle
rect = Rectangle(5, 3)
print(rect.aire())  # 15

"""
[OK] Avantages :
1. Impossible de créer Forme directement
2. Erreur à la définition si méthode manquante
3. Contrat clair pour sous-classes
"""


# ----------------------------------------------------------------------------
# [PACKAGE] MODULE abc : ABSTRACT BASE CLASSES
# ----------------------------------------------------------------------------

"""
IMPORTER ABC
"""

from abc import ABC, abstractmethod

"""
[IDEE] ABC = Abstract Base Class

abc est un module Python standard
Fournit :
- ABC : Classe de base pour classes abstraites
- @abstractmethod : Décorateur pour méthodes abstraites
- @abstractproperty : Pour propriétés abstraites (déprécié, voir plus bas)


SYNTAXE COMPLÈTE
"""

from abc import ABC, abstractmethod

class ClasseAbstraite(ABC):  # <- Hériter de ABC
    
    @abstractmethod  # <- Décorateur obligatoire
    def methode_abstraite(self):
        """
        Méthode abstraite : DOIT être implémentée dans sous-classes
        
        Le corps peut :
        - Être pass
        - Contenir implémentation par défaut (optionnel)
        - Lever NotImplementedError (optionnel)
        """
        pass
    
    def methode_concrete(self):
        """
        Méthode concrète : Implémentation fournie
        Sous-classes peuvent l'utiliser telle quelle ou la surcharger
        """
        print("Implémentation concrète")

"""
EXEMPLE COMPLET : SYSTÈME DE PAIEMENT
"""

from abc import ABC, abstractmethod

class Paiement(ABC):
    """Classe abstraite pour systèmes de paiement"""
    
    def __init__(self, montant):
        self.montant = montant
        self.statut = "En attente"
    
    @abstractmethod
    def traiter(self):
        """Traiter le paiement (à implémenter)"""
        pass
    
    @abstractmethod
    def annuler(self):
        """Annuler le paiement (à implémenter)"""
        pass
    
    def afficher_statut(self):
        """Méthode concrète (implémentation fournie)"""
        print(f"Montant: {self.montant}€ - Statut: {self.statut}")

# [X] Impossible de créer Paiement directement
# p = Paiement(100)  # TypeError

# [OK] Créer sous-classes concrètes

class PaiementCarte(Paiement):
    def __init__(self, montant, numero_carte):
        super().__init__(montant)
        self.numero_carte = numero_carte
    
    def traiter(self):  # <- Implémentation obligatoire
        print(f"Traitement carte {self.numero_carte}: {self.montant}€")
        self.statut = "Traité"
    
    def annuler(self):  # <- Implémentation obligatoire
        print(f"Annulation carte {self.numero_carte}")
        self.statut = "Annulé"

class PaiementVirement(Paiement):
    def __init__(self, montant, iban):
        super().__init__(montant)
        self.iban = iban
    
    def traiter(self):
        print(f"Traitement virement {self.iban}: {self.montant}€")
        self.statut = "Traité"
    
    def annuler(self):
        print(f"Annulation virement {self.iban}")
        self.statut = "Annulé"

# Test
paiements = [
    PaiementCarte(99.99, "1234-****-****-5678"),
    PaiementVirement(150.00, "FR76 XXXX XXXX XXXX XXXX XXXX XXX")
]

for p in paiements:
    p.afficher_statut()
    p.traiter()
    p.afficher_statut()
    print()

"""
[ATTENTION] SOUS-CLASSE INCOMPLÈTE = ERREUR
"""

class PaiementIncomplet(Paiement):
    def traiter(self):
        print("Traité")
    
    # [X] Oublié annuler() !

# Erreur à la définition (pas à l'exécution !)
# p = PaiementIncomplet(50)
# TypeError: Can't instantiate abstract class PaiementIncomplet
# with abstract method annuler


# ----------------------------------------------------------------------------
# [DESIGN] MÉTHODES ABSTRAITES AVEC IMPLÉMENTATION
# ----------------------------------------------------------------------------

"""
MÉTHODE ABSTRAITE PEUT AVOIR IMPLÉMENTATION PAR DÉFAUT

Sous-classes DOIVENT la surcharger,
mais PEUVENT appeler super() pour réutiliser code parent
"""

from abc import ABC, abstractmethod

class Vehicule(ABC):
    def __init__(self, marque):
        self.marque = marque
        self.vitesse = 0
    
    @abstractmethod
    def demarrer(self):
        """Démarrage générique (à surcharger)"""
        print(f"Démarrage du véhicule {self.marque}")
    
    @abstractmethod
    def accelerer(self, increment):
        """Accélération générique (à surcharger)"""
        self.vitesse += increment
        print(f"Vitesse: {self.vitesse} km/h")

class Voiture(Vehicule):
    def demarrer(self):
        # [OK] Peut appeler implémentation parent
        super().demarrer()
        print("Vérification ceintures")
        print("Moteur démarré")
    
    def accelerer(self, increment):
        # [OK] Peut appeler implémentation parent
        super().accelerer(increment)
        print("Vroum!")

class Moto(Vehicule):
    def demarrer(self):
        super().demarrer()
        print("Béquille relevée")
    
    def accelerer(self, increment):
        super().accelerer(increment * 1.5)  # Moto accélère plus vite
        print("Braaap!")

# Test
v = Voiture("Toyota")
v.demarrer()
# Démarrage du véhicule Toyota
# Vérification ceintures
# Moteur démarré

v.accelerer(20)
# Vitesse: 20 km/h
# Vroum!

"""
[IDEE] PATTERN UTILE

Classe abstraite fournit comportement par défaut
Sous-classes peuvent :
- L'utiliser via super()
- L'étendre
- Le remplacer complètement
"""


# ----------------------------------------------------------------------------
# [LABEL] PROPRIÉTÉS ABSTRAITES
# ----------------------------------------------------------------------------

"""
PROPRIÉTÉ ABSTRAITE : Propriété à implémenter dans sous-classes
"""

from abc import ABC, abstractmethod

class Forme(ABC):
    @property
    @abstractmethod
    def aire(self):
        """Aire de la forme (propriété abstraite)"""
        pass
    
    @property
    @abstractmethod
    def perimetre(self):
        """Périmètre de la forme (propriété abstraite)"""
        pass
    
    def afficher_info(self):
        """Méthode concrète utilisant propriétés abstraites"""
        print(f"Aire: {self.aire:.2f}")
        print(f"Périmètre: {self.perimetre:.2f}")

class Rectangle(Forme):
    def __init__(self, largeur, hauteur):
        self._largeur = largeur
        self._hauteur = hauteur
    
    @property
    def aire(self):  # <- Implémentation de propriété abstraite
        return self._largeur * self._hauteur
    
    @property
    def perimetre(self):
        return 2 * (self._largeur + self._hauteur)

class Cercle(Forme):
    def __init__(self, rayon):
        self._rayon = rayon
    
    @property
    def aire(self):
        import math
        return math.pi * self._rayon ** 2
    
    @property
    def perimetre(self):
        import math
        return 2 * math.pi * self._rayon

# Test
formes = [
    Rectangle(5, 3),
    Cercle(4)
]

for forme in formes:
    print(f"{forme.__class__.__name__}:")
    forme.afficher_info()
    print()

"""
[IDEE] ORDRE DES DÉCORATEURS

@property DOIT être AVANT @abstractmethod
"""

# [OK] Correct
@property
@abstractmethod
def aire(self):
    pass

# [X] Incorrect
@abstractmethod
@property
def aire(self):
    pass


# ----------------------------------------------------------------------------
# [OBJECTIF] INTERFACES AVEC ABC
# ----------------------------------------------------------------------------

"""
INTERFACE = Classe abstraite avec SEULEMENT méthodes abstraites
           (pas d'implémentation)

Pattern commun en programmation orientée objet
"""

from abc import ABC, abstractmethod

class Comparable(ABC):
    """Interface pour objets comparables"""
    
    @abstractmethod
    def __eq__(self, other):
        pass
    
    @abstractmethod
    def __lt__(self, other):
        pass
    
    @abstractmethod
    def __le__(self, other):
        pass
    
    @abstractmethod
    def __gt__(self, other):
        pass
    
    @abstractmethod
    def __ge__(self, other):
        pass

class Serializable(ABC):
    """Interface pour objets sérialisables"""
    
    @abstractmethod
    def to_json(self):
        """Convertir en JSON"""
        pass
    
    @abstractmethod
    def from_json(self, json_str):
        """Créer depuis JSON"""
        pass

class Personne(Comparable, Serializable):
    """Personne comparable et sérialisable"""
    
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
    
    # Implémentation Comparable
    def __eq__(self, other):
        return self.age == other.age
    
    def __lt__(self, other):
        return self.age < other.age
    
    def __le__(self, other):
        return self.age <= other.age
    
    def __gt__(self, other):
        return self.age > other.age
    
    def __ge__(self, other):
        return self.age >= other.age
    
    # Implémentation Serializable
    def to_json(self):
        import json
        return json.dumps({'nom': self.nom, 'age': self.age})
    
    def from_json(self, json_str):
        import json
        data = json.loads(json_str)
        self.nom = data['nom']
        self.age = data['age']
    
    def __str__(self):
        return f"{self.nom} ({self.age} ans)"

# Test
personnes = [
    Personne("Alice", 30),
    Personne("Bob", 25),
    Personne("Charlie", 35)
]

# Comparable fonctionne
personnes_triees = sorted(personnes)
for p in personnes_triees:
    print(p)

# Serializable fonctionne
p = personnes[0]
json_str = p.to_json()
print(f"\nJSON: {json_str}")

"""
[IDEE] INTERFACES MULTIPLES

Python permet d'hériter de plusieurs classes abstraites
= Implémenter plusieurs interfaces
"""


# ----------------------------------------------------------------------------
# [OUTIL] VÉRIFIER IMPLÉMENTATION
# ----------------------------------------------------------------------------

"""
VÉRIFIER SI CLASSE IMPLÉMENTE INTERFACE
"""

from abc import ABC

# Vérifier si classe hérite d'une classe abstraite
print(issubclass(Rectangle, Forme))  # True
print(issubclass(Personne, Comparable))  # True

# Vérifier si instance
rect = Rectangle(5, 3)
print(isinstance(rect, Forme))  # True

"""
MÉTHODES ABSTRAITES D'UNE CLASSE
"""

class MaClasse(ABC):
    @abstractmethod
    def methode1(self):
        pass
    
    @abstractmethod
    def methode2(self):
        pass
    
    def methode_concrete(self):
        pass

# Lister méthodes abstraites
print(MaClasse.__abstractmethods__)
# frozenset({'methode1', 'methode2'})


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 4 : SYSTÈME DE NOTIFICATION
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer système de notification avec classes abstraites

SPÉCIFICATIONS :

1. Classe abstraite Notification :
   - Attributs : message, destinataire
   - Méthode abstraite : envoyer()
   - Méthode concrète : valider()

2. Sous-classes concrètes :
   - NotificationEmail
   - NotificationSMS
   - NotificationPush

3. Fonction traiter_notifications() utilisant polymorphisme


SOLUTION :
"""

from abc import ABC, abstractmethod
import re

class Notification(ABC):
    """Classe abstraite pour notifications"""
    
    def __init__(self, destinataire, message):
        self.destinataire = destinataire
        self.message = message
        self.envoyee = False
    
    @abstractmethod
    def envoyer(self):
        """Envoyer la notification (à implémenter)"""
        pass
    
    @abstractmethod
    def valider_destinataire(self):
        """Valider format du destinataire (à implémenter)"""
        pass
    
    def valider(self):
        """Valider la notification avant envoi"""
        if not self.message:
            raise ValueError("Message vide")
        
        if not self.valider_destinataire():
            raise ValueError("Destinataire invalide")
        
        return True
    
    def afficher_statut(self):
        """Afficher statut de la notification"""
        statut = "Envoyée" if self.envoyee else "En attente"
        print(f"[{self.__class__.__name__}] {self.destinataire}: {statut}")

class NotificationEmail(Notification):
    """Notification par email"""
    
    def valider_destinataire(self):
        """Vérifier format email"""
        pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
        return re.match(pattern, self.destinataire) is not None
    
    def envoyer(self):
        """Envoyer email"""
        if self.valider():
            print(f"[EMAIL] Envoi email à {self.destinataire}")
            print(f"   Sujet: Notification")
            print(f"   Corps: {self.message}")
            self.envoyee = True

class NotificationSMS(Notification):
    """Notification par SMS"""
    
    def valider_destinataire(self):
        """Vérifier format numéro (simplifié)"""
        # Format : +33612345678 ou 0612345678
        pattern = r'^(\+33|0)[67]\d{8}$'
        return re.match(pattern, self.destinataire) is not None
    
    def envoyer(self):
        """Envoyer SMS"""
        if self.valider():
            # Limiter message à 160 caractères
            message_court = self.message[:160]
            print(f"[MOBILE] Envoi SMS à {self.destinataire}")
            print(f"   Message: {message_court}")
            if len(self.message) > 160:
                print(f"   (tronqué, {len(self.message)} caractères)")
            self.envoyee = True

class NotificationPush(Notification):
    """Notification push (mobile)"""
    
    def __init__(self, destinataire, message, titre="Notification"):
        super().__init__(destinataire, message)
        self.titre = titre
    
    def valider_destinataire(self):
        """Vérifier format device token (simplifié)"""
        # Device token = chaîne alphanumérique de 64 caractères
        return len(self.destinataire) == 64 and self.destinataire.isalnum()
    
    def envoyer(self):
        """Envoyer notification push"""
        if self.valider():
            print(f"[NOTIF] Push vers appareil {self.destinataire[:8]}...")
            print(f"   Titre: {self.titre}")
            print(f"   Message: {self.message}")
            self.envoyee = True

def traiter_notifications(notifications):
    """
    Traiter une liste de notifications (polymorphisme)
    
    Args:
        notifications: Liste d'objets Notification
    """
    print("=== TRAITEMENT DES NOTIFICATIONS ===\n")
    
    for notif in notifications:
        try:
            notif.envoyer()
            print()
        except ValueError as e:
            print(f"[X] Erreur: {e}")
            print()

# Test complet
notifications = [
    NotificationEmail(
        "alice@example.com",
        "Votre commande a été expédiée !"
    ),
    NotificationSMS(
        "0612345678",
        "Code de vérification: 123456"
    ),
    NotificationPush(
        "a" * 64,  # Device token simulé
        "Nouveau message reçu",
        "Application"
    ),
    NotificationEmail(
        "email-invalide",  # [X] Email invalide
        "Test"
    ),
    NotificationSMS(
        "0612345678",
        "Message très très long " * 20  # [ATTENTION] Sera tronqué
    )
]

# Traiter toutes les notifications
traiter_notifications(notifications)

# Afficher statuts finaux
print("\n=== STATUTS FINAUX ===")
for notif in notifications:
    notif.afficher_statut()

"""
Sortie :
=== TRAITEMENT DES NOTIFICATIONS ===

[EMAIL] Envoi email à alice@example.com
   Sujet: Notification
   Corps: Votre commande a été expédiée !

[MOBILE] Envoi SMS à 0612345678
   Message: Code de vérification: 123456

[NOTIF] Push vers appareil aaaaaaaa...
   Titre: Application
   Message: Nouveau message reçu

[X] Erreur: Destinataire invalide

[MOBILE] Envoi SMS à 0612345678
   Message: Message très très long Message très très long Message très très long Message très très long Message très très long Message très très long Message très très long
   (tronqué, 420 caractères)


=== STATUTS FINAUX ===
[NotificationEmail] alice@example.com: Envoyée
[NotificationSMS] 0612345678: Envoyée
[NotificationPush] aaaaaaaaaa...: Envoyée
[NotificationEmail] email-invalide: En attente
[NotificationSMS] 0612345678: Envoyée
"""


# ============================================================================
# [DOCS] RÉCAPITULATIF PARTIE 2 COMPLÈTE
# ============================================================================

"""
[BRAVO] FÉLICITATIONS ! PARTIE 2 TERMINÉE !

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 5 : Héritage Simple
[OK] Concept d'héritage (réutilisation de code)
[OK] Syntaxe : class Enfant(Parent):
[OK] Surcharge de méthodes (override)
[OK] super() pour appeler méthodes parent
[OK] Étendre méthodes parent
[OK] isinstance() et issubclass()
[OK] MRO (Method Resolution Order)

Chapitre 6 : Héritage Multiple
[OK] Hériter de plusieurs classes
[OK] MRO avec héritage multiple
[OK] Problème du diamant
[OK] Solution avec super() et **kwargs
[OK] Mix-ins (ajouter fonctionnalités)
[OK] Quand éviter héritage multiple
[OK] Composition vs Héritage

Chapitre 7 : Polymorphisme
[OK] Concept de polymorphisme
[OK] Polymorphisme par héritage
[OK] Duck typing ("si ça marche comme un canard...")
[OK] Protocoles Python
[OK] Interfaces implicites
[OK] hasattr() pour vérifier capacités

Chapitre 8 : Classes Abstraites
[OK] Module abc (Abstract Base Classes)
[OK] Classe abstraite avec ABC
[OK] @abstractmethod
[OK] Propriétés abstraites
[OK] Forcer implémentation dans sous-classes
[OK] Interfaces avec ABC
[OK] Héritage multiple d'interfaces


[CLE] COMPÉTENCES ACQUISES

Vous pouvez maintenant créer :
[OK] Hiérarchies de classes claires
[OK] Code réutilisable via héritage
[OK] Interfaces et contrats avec ABC
[OK] Systèmes extensibles (ajout facile de nouvelles classes)
[OK] Code polymorphique (même interface, comportements différents)
[OK] Mix-ins pour ajouter fonctionnalités


[OBJECTIF] PATTERNS MAÎTRISÉS

Pattern Template Method :
- Classe abstraite définit squelette
- Sous-classes implémentent détails

Pattern Strategy :
- Différentes implémentations d'une interface
- Interchangeables

Pattern Factory :
- @classmethod pour créer instances


-> PROCHAINE ÉTAPE : PARTIE 3 - CONCEPTS AVANCÉS

La Partie 3 couvrira :
- Encapsulation avancée
- Descripteurs et métaclasses
- Dataclasses (@dataclass)
- Composition détaillée
- Délégation
- Patterns avancés

Vous allez apprendre à :
- Maîtriser encapsulation complète
- Utiliser dataclasses efficacement
- Préférer composition quand approprié
- Créer code maintenable et élégant


[GUIDE] FICHIER SUIVANT : poo_partie3.txt

Excellent travail ! Continuez ! [RAPIDE]


═══════════════════════════════════════════════════════════════════════════
EXERCICES DE RÉVISION PARTIE 2
═══════════════════════════════════════════════════════════════════════════

EXERCICE 1 : HIÉRARCHIE D'EMPLOYÉS
───────────────────────────────────

Créez :
1. Classe abstraite Employe avec :
   - Attributs : nom, salaire_base
   - Méthode abstraite : calculer_salaire()
   - Méthode concrète : afficher_info()

2. Sous-classes :
   - Developpeur (heures supplémentaires)
   - Manager (bonus fixe)
   - Commercial (commission sur ventes)


EXERCICE 2 : SYSTÈME DE FICHIERS
─────────────────────────────────

Créez :
1. Classe abstraite ElementSysteme avec :
   - Attributs : nom, taille
   - Méthodes abstraites : afficher(), supprimer()

2. Sous-classes :
   - Fichier (contenu)
   - Dossier (liste de sous-éléments)


EXERCICE 3 : MIX-INS
────────────────────

Créez mix-ins pour :
1. LogMixin : Ajouter logging
2. TimestampMixin : Ajouter created_at, updated_at
3. SerializableMixin : to_dict() et from_dict()

Utilisez-les dans classe Produit


SOLUTIONS DISPONIBLES DANS LE DÉPÔT
"""

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 3 : CONCEPTS AVANCÉS
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 9 : Encapsulation Avancée
# - Chapitre 10 : Dataclasses
# - Chapitre 11 : Composition vs Héritage
# - Chapitre 12 : Descripteurs
# - Chapitre 13 : Métaclasses (Introduction)
#
# [TEMPS] TEMPS : ~8-10 heures
# [DOCS] PRÉREQUIS : Parties 1 et 2 complétées
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 9 : ENCAPSULATION AVANCÉE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Maîtriser l'encapsulation en profondeur
[OK] Attributs privés avec name mangling
[OK] Properties getter/setter/deleter avancés
[OK] __slots__ pour optimiser mémoire
[OK] Immutabilité d'objets
[OK] Pattern de design : Encapsulation forte
"""


# ----------------------------------------------------------------------------
# [SECURISE] ENCAPSULATION : RAPPELS ET APPROFONDISSEMENT
# ----------------------------------------------------------------------------

"""
ENCAPSULATION = Cacher détails internes, exposer interface publique

[IDEE] Niveaux d'accès en Python :

public      : nom          (accessible partout)
protégé     : _nom         (convention : usage interne)
privé       : __nom        (name mangling)


ATTRIBUTS PUBLICS
"""

class Personne:
    def __init__(self, nom):
        self.nom = nom  # <- Public

p = Personne("Alice")
print(p.nom)  # [OK] Accessible
p.nom = "Bob"  # [OK] Modifiable

"""
ATTRIBUTS PROTÉGÉS (Convention)
"""

class CompteBancaire:
    def __init__(self, solde):
        self._solde = solde  # <- Protégé (convention)
        # "_" signifie : "Usage interne, ne touchez pas"

compte = CompteBancaire(1000)
print(compte._solde)  # [OK] Accessible (mais déconseillé)

"""
[IDEE] Note : "_" est juste une CONVENTION
Python ne bloque PAS l'accès
C'est au développeur de respecter la convention


ATTRIBUTS PRIVÉS (Name Mangling)
"""

class CompteBancaire:
    def __init__(self, solde):
        self.__solde = solde  # <- Privé (name mangling)
    
    def get_solde(self):
        return self.__solde

compte = CompteBancaire(1000)

# [X] Accès direct impossible
try:
    print(compte.__solde)
except AttributeError as e:
    print(f"Erreur: {e}")
# Erreur: 'CompteBancaire' object has no attribute '__solde'

# [OK] Via méthode publique
print(compte.get_solde())  # 1000

"""
[IDEE] NAME MANGLING

Python renomme __attribut en _NomClasse__attribut

C'est toujours accessible si on connaît le "vrai" nom :
"""

compte = CompteBancaire(1000)
print(compte._CompteBancaire__solde)  # 1000 (accès possible mais très déconseillé)

"""
[ATTENTION] NAME MANGLING N'EST PAS UNE VRAIE SÉCURITÉ

But : Éviter conflits de noms dans héritage
Pas pour "cacher" vraiment
"""


# ----------------------------------------------------------------------------
# [DESIGN] PROPERTIES : GETTER/SETTER/DELETER AVANCÉS
# ----------------------------------------------------------------------------

"""
PROPERTY COMPLÈTE : getter + setter + deleter
"""

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    
    @property
    def celsius(self):
        """Getter : Lire la valeur"""
        print("Lecture température")
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        """Setter : Modifier avec validation"""
        print(f"Modification température: {value}")
        if value < -273.15:
            raise ValueError("Température impossible (< zéro absolu)")
        self._celsius = value
    
    @celsius.deleter
    def celsius(self):
        """Deleter : Supprimer l'attribut"""
        print("Suppression température")
        del self._celsius

# Test
temp = Temperature(25)

# Getter
print(temp.celsius)  # Lecture température + 25

# Setter
temp.celsius = 30    # Modification température: 30

# Deleter
del temp.celsius     # Suppression température

"""
PROPERTY AVEC CACHE (Lazy Loading)
"""

class Image:
    def __init__(self, chemin):
        self.chemin = chemin
        self._data = None  # Cache
    
    @property
    def data(self):
        """
        [IDEE] LAZY LOADING
        
        Charger données seulement quand nécessaire
        Mettre en cache pour éviter rechargements
        """
        if self._data is None:
            print(f"Chargement de {self.chemin}...")
            # Simuler chargement long
            self._data = f"Données de {self.chemin}"
        return self._data

# Test
img = Image("photo.jpg")
print("Image créée (pas encore chargée)")

print(img.data)  # Chargement de photo.jpg... + Données de photo.jpg
print(img.data)  # Données de photo.jpg (pas de rechargement !)

"""
PROPERTY CALCULÉE (READ-ONLY)
"""

class Rectangle:
    def __init__(self, largeur, hauteur):
        self.largeur = largeur
        self.hauteur = hauteur
    
    @property
    def aire(self):
        """
        [IDEE] PROPERTY CALCULÉE
        
        Pas de setter : lecture seule
        Calculée dynamiquement
        """
        return self.largeur * self.hauteur
    
    @property
    def perimetre(self):
        return 2 * (self.largeur + self.hauteur)

# Test
rect = Rectangle(5, 3)
print(rect.aire)      # 15
print(rect.perimetre) # 16

# [X] Impossible de modifier
try:
    rect.aire = 20
except AttributeError as e:
    print(f"Erreur: {e}")
# Erreur: can't set attribute

"""
PROPERTY AVEC VALIDATION COMPLEXE
"""

class Utilisateur:
    def __init__(self, email, age):
        self.email = email  # Utilise le setter
        self.age = age      # Utilise le setter
    
    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, value):
        """Validation email"""
        import re
        pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
        if not re.match(pattern, value):
            raise ValueError(f"Email invalide: {value}")
        self._email = value
    
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        """Validation age"""
        if not isinstance(value, int):
            raise TypeError("Age doit être un entier")
        if value < 0 or value > 150:
            raise ValueError(f"Age invalide: {value}")
        self._age = value

# Test
user = Utilisateur("alice@example.com", 30)

# [OK] Valeurs valides
user.email = "bob@test.com"
user.age = 25

# [X] Valeurs invalides
try:
    user.email = "email-invalide"
except ValueError as e:
    print(f"Erreur email: {e}")

try:
    user.age = 200
except ValueError as e:
    print(f"Erreur age: {e}")


# ----------------------------------------------------------------------------
# [SAUVEGARDE] __slots__ : OPTIMISATION MÉMOIRE
# ----------------------------------------------------------------------------

"""
PROBLÈME : DICTIONNAIRE PAR INSTANCE

Par défaut, chaque instance a un __dict__ :
"""

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
print(p.__dict__)  # {'x': 1, 'y': 2}

# Peut ajouter attributs dynamiquement
p.z = 3  # [OK] Possible

"""
Avantages : Flexibilité
Inconvénients : Consomme beaucoup de mémoire


SOLUTION : __slots__

Définir explicitement les attributs autorisés
"""

class Point:
    __slots__ = ['x', 'y']  # <- Seulement x et y
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)

# [OK] Attributs définis
print(p.x, p.y)  # 1 2

# [X] Pas de __dict__
try:
    print(p.__dict__)
except AttributeError:
    print("Pas de __dict__ avec __slots__")

# [X] Impossible d'ajouter attributs
try:
    p.z = 3
except AttributeError as e:
    print(f"Erreur: {e}")
# Erreur: 'Point' object has no attribute 'z'

"""
[IDEE] AVANTAGES DE __slots__ :

1. MÉMOIRE
   Environ 40-50% moins de mémoire par instance

2. PERFORMANCE
   Accès aux attributs plus rapide

3. PROTECTION
   Empêche ajout d'attributs non prévus


[ATTENTION] INCONVÉNIENTS :

1. PAS DE __dict__
   Pas d'attributs dynamiques

2. PAS DE __weakref__
   Par défaut (peut ajouter '__weakref__' dans __slots__)

3. HÉRITAGE COMPLEXE
   Chaque classe doit définir __slots__


QUAND UTILISER __slots__ :

[OK] Classes avec BEAUCOUP d'instances (millions)
[OK] Attributs connus à l'avance
[OK] Optimisation mémoire critique

[X] Classes "normales"
[X] Besoin de flexibilité
[X] Attributs dynamiques nécessaires
"""

# Comparaison mémoire
import sys

class SansSlots:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class AvecSlots:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

sans = SansSlots(1, 2)
avec = AvecSlots(1, 2)

print(f"Sans __slots__: {sys.getsizeof(sans.__dict__)} bytes")
print(f"Avec __slots__: {sys.getsizeof(avec)} bytes")


# ----------------------------------------------------------------------------
# [VERROUILLE] IMMUTABILITÉ
# ----------------------------------------------------------------------------

"""
OBJET IMMUTABLE = Objet dont l'état ne peut pas changer après création

Exemples Python : str, int, float, tuple


CRÉER CLASSE IMMUTABLE
"""

class Point:
    """Point immutable"""
    
    __slots__ = ['_x', '_y']  # Optimisation + limite attributs
    
    def __init__(self, x, y):
        # Initialisation normale
        object.__setattr__(self, '_x', x)
        object.__setattr__(self, '_y', y)
    
    @property
    def x(self):
        """Lecture seule"""
        return self._x
    
    @property
    def y(self):
        """Lecture seule"""
        return self._y
    
    def __setattr__(self, name, value):
        """
        [IDEE] BLOQUER TOUTE MODIFICATION
        
        Empêche p.x = 5
        """
        raise AttributeError("Point est immutable")
    
    def __delattr__(self, name):
        """Bloquer suppression d'attributs"""
        raise AttributeError("Point est immutable")
    
    def __repr__(self):
        return f"Point({self.x}, {self.y})"

# Test
p = Point(1, 2)
print(p)  # Point(1, 2)

# [OK] Lecture OK
print(p.x, p.y)  # 1 2

# [X] Modification impossible
try:
    p.x = 5
except AttributeError as e:
    print(f"Erreur: {e}")
# Erreur: Point est immutable

"""
[IDEE] AVANTAGES DE L'IMMUTABILITÉ :

1. THREAD-SAFE
   Pas de problèmes de concurrence

2. HASHABLE
   Peut être clé de dict ou dans set

3. PRÉVISIBLE
   État ne change jamais

4. CACHE-FRIENDLY
   Résultats peuvent être cachés


OBJET IMMUTABLE HASHABLE
"""

class Point:
    __slots__ = ['_x', '_y']
    
    def __init__(self, x, y):
        object.__setattr__(self, '_x', x)
        object.__setattr__(self, '_y', y)
    
    @property
    def x(self):
        return self._x
    
    @property
    def y(self):
        return self._y
    
    def __setattr__(self, name, value):
        raise AttributeError("Point est immutable")
    
    def __eq__(self, other):
        if not isinstance(other, Point):
            return False
        return self.x == other.x and self.y == other.y
    
    def __hash__(self):
        """
        [IDEE] HASHABLE car immutable
        
        Objets égaux doivent avoir même hash
        """
        return hash((self.x, self.y))
    
    def __repr__(self):
        return f"Point({self.x}, {self.y})"

# Test
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

# Utilisation dans set
points = {p1, p2, p3}  # p1 et p2 sont dédupliqués
print(points)  # {Point(1, 2), Point(3, 4)}

# Utilisation comme clé de dict
distances = {
    Point(0, 0): 0,
    Point(1, 1): 1.414,
    Point(2, 2): 2.828
}
print(distances[Point(1, 1)])  # 1.414


# ============================================================================
# [GUIDE] CHAPITRE 10 : DATACLASSES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce qu'une dataclass
[OK] Utiliser @dataclass
[OK] Avantages vs classes normales
[OK] Options de dataclass
[OK] Validation et post-init
[OK] Immutabilité avec frozen=True
"""


# ----------------------------------------------------------------------------
# [PACKAGE] QU'EST-CE QU'UNE DATACLASS ?
# ----------------------------------------------------------------------------

"""
DATACLASS = Classe principalement pour STOCKER DONNÉES

Introduit en Python 3.7
Réduit code boilerplate (répétitif)


SANS DATACLASS (Code répétitif)
"""

class Personne:
    def __init__(self, nom, age, ville):
        self.nom = nom
        self.age = age
        self.ville = ville
    
    def __repr__(self):
        return f"Personne(nom={self.nom!r}, age={self.age!r}, ville={self.ville!r})"
    
    def __eq__(self, other):
        if not isinstance(other, Personne):
            return False
        return (self.nom == other.nom and 
                self.age == other.age and 
                self.ville == other.ville)

"""
AVEC DATACLASS (Beaucoup plus simple !)
"""

from dataclasses import dataclass

@dataclass
class Personne:
    """
    [IDEE] DATACLASS
    
    @dataclass génère automatiquement :
    - __init__
    - __repr__
    - __eq__
    - Et plus selon options
    """
    nom: str
    age: int
    ville: str

# Test
p1 = Personne("Alice", 30, "Paris")
print(p1)  # Personne(nom='Alice', age=30, ville='Paris')

p2 = Personne("Alice", 30, "Paris")
print(p1 == p2)  # True

"""
[OK] MÊME FONCTIONNALITÉ
[OK] BEAUCOUP MOINS DE CODE
"""


# ----------------------------------------------------------------------------
# [DESIGN] UTILISATION DES DATACLASSES
# ----------------------------------------------------------------------------

"""
DÉCLARATION BASIQUE
"""

from dataclasses import dataclass

@dataclass
class Produit:
    nom: str
    prix: float
    stock: int

# Création
p = Produit("Laptop", 999.99, 10)
print(p)  # Produit(nom='Laptop', prix=999.99, stock=10)

"""
VALEURS PAR DÉFAUT
"""

from dataclasses import dataclass

@dataclass
class Produit:
    nom: str
    prix: float
    stock: int = 0  # <- Valeur par défaut
    actif: bool = True

p1 = Produit("Laptop", 999.99)
print(p1)  # Produit(nom='Laptop', prix=999.99, stock=0, actif=True)

p2 = Produit("Mouse", 29.99, 50, False)
print(p2)  # Produit(nom='Mouse', prix=29.99, stock=50, actif=False)

"""
[ATTENTION] VALEURS PAR DÉFAUT MUTABLES

Piège classique :
"""

from dataclasses import dataclass, field

# [X] INCORRECT
@dataclass
class MauvaiseClasse:
    items: list = []  # [X] Erreur !

# ValueError: mutable default <class 'list'> for field items is not allowed

# [OK] CORRECT
@dataclass
class BonneClasse:
    items: list = field(default_factory=list)  # [OK] Utiliser field()

# Test
b1 = BonneClasse()
b2 = BonneClasse()

b1.items.append("A")
print(b1.items)  # ['A']
print(b2.items)  # [] (listes séparées !)

"""
[IDEE] field() AVEC DEFAULT_FACTORY

default_factory : Fonction appelée pour créer valeur par défaut
Utilisé pour valeurs mutables (list, dict, set)
"""


# ----------------------------------------------------------------------------
# [CONFIG] OPTIONS DE DATACLASS
# ----------------------------------------------------------------------------

"""
@dataclass ACCEPTE PLUSIEURS OPTIONS
"""

from dataclasses import dataclass

@dataclass(
    init=True,       # Générer __init__ (défaut: True)
    repr=True,       # Générer __repr__ (défaut: True)
    eq=True,         # Générer __eq__ (défaut: True)
    order=False,     # Générer __lt__, __le__, __gt__, __ge__ (défaut: False)
    frozen=False,    # Rendre immutable (défaut: False)
    unsafe_hash=False  # Générer __hash__ (défaut: False)
)
class MaClasse:
    pass

"""
OPTION order=True : COMPARAISONS
"""

from dataclasses import dataclass

@dataclass(order=True)
class Personne:
    nom: str
    age: int

# Comparaisons activées
p1 = Personne("Alice", 30)
p2 = Personne("Bob", 25)

print(p1 > p2)   # True (compare par ordre des champs)
print(p1 < p2)   # False

# Tri possible
personnes = [
    Personne("Charlie", 35),
    Personne("Alice", 30),
    Personne("Bob", 25)
]

personnes_triees = sorted(personnes)
for p in personnes_triees:
    print(p)
# Personne(nom='Alice', age=30)
# Personne(nom='Bob', age=25)
# Personne(nom='Charlie', age=35)

"""
OPTION frozen=True : IMMUTABILITÉ
"""

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    """Point immutable"""
    x: float
    y: float

p = Point(1.0, 2.0)
print(p)  # Point(x=1.0, y=2.0)

# [X] Impossible de modifier
try:
    p.x = 5.0
except AttributeError as e:
    print(f"Erreur: {e}")
# Erreur: cannot assign to field 'x'

# [OK] Hashable (peut être clé de dict)
points = {Point(0, 0): "origine", Point(1, 1): "diagonale"}
print(points[Point(0, 0)])  # origine


# ----------------------------------------------------------------------------
# [OUTILS] field() : CONFIGURATION AVANCÉE
# ----------------------------------------------------------------------------

"""
field() PERMET PERSONNALISATION PAR CHAMP
"""

from dataclasses import dataclass, field

@dataclass
class Utilisateur:
    nom: str
    age: int
    email: str
    _id: str = field(default_factory=lambda: str(uuid.uuid4()), repr=False)
    # ^ Généré automatiquement, pas affiché dans repr

# Test
import uuid
u = Utilisateur("Alice", 30, "alice@example.com")
print(u)  # Utilisateur(nom='Alice', age=30, email='alice@example.com')
# _id existe mais n'est pas affiché

"""
PARAMÈTRES DE field()
"""

from dataclasses import dataclass, field

@dataclass
class Exemple:
    # default : Valeur par défaut
    nom: str = field(default="Sans nom")
    
    # default_factory : Fonction créant valeur par défaut
    items: list = field(default_factory=list)
    
    # init : Inclure dans __init__ ?
    interne: str = field(default="", init=False)
    
    # repr : Inclure dans __repr__ ?
    secret: str = field(default="", repr=False)
    
    # compare : Utiliser pour __eq__ ?
    timestamp: float = field(default=0.0, compare=False)

"""
CHAMPS CALCULÉS (post_init)
"""

from dataclasses import dataclass

@dataclass
class Rectangle:
    largeur: float
    hauteur: float
    aire: float = field(init=False)  # <- Pas dans __init__
    
    def __post_init__(self):
        """
        [IDEE] __post_init__
        
        Appelé automatiquement APRÈS __init__
        Pour initialiser champs calculés
        """
        self.aire = self.largeur * self.hauteur

# Test
rect = Rectangle(5, 3)
print(rect)  # Rectangle(largeur=5, hauteur=3, aire=15)

"""
VALIDATION DANS __post_init__
"""

from dataclasses import dataclass

@dataclass
class Produit:
    nom: str
    prix: float
    
    def __post_init__(self):
        """Validation après initialisation"""
        if not self.nom:
            raise ValueError("Nom ne peut pas être vide")
        if self.prix < 0:
            raise ValueError("Prix ne peut pas être négatif")

# Test
p1 = Produit("Laptop", 999.99)  # [OK] OK

try:
    p2 = Produit("", 100)  # [X] Nom vide
except ValueError as e:
    print(f"Erreur: {e}")

try:
    p3 = Produit("Mouse", -10)  # [X] Prix négatif
except ValueError as e:
    print(f"Erreur: {e}")


# ----------------------------------------------------------------------------
# [COURS] EXERCICE : DATACLASS COMPLÈTE
# ----------------------------------------------------------------------------

"""
CRÉER SYSTÈME DE COMMANDE AVEC DATACLASSES
"""

from dataclasses import dataclass, field
from datetime import datetime
from typing import List
import uuid

@dataclass(order=True)
class Article:
    """Article de catalogue"""
    nom: str
    prix: float
    stock: int = 0
    
    def __post_init__(self):
        if self.prix < 0:
            raise ValueError("Prix négatif")
        if self.stock < 0:
            raise ValueError("Stock négatif")

@dataclass
class LigneCommande:
    """Ligne dans une commande"""
    article: Article
    quantite: int
    
    @property
    def total(self):
        """Prix total de la ligne"""
        return self.article.prix * self.quantite
    
    def __post_init__(self):
        if self.quantite <= 0:
            raise ValueError("Quantité doit être positive")
        if self.quantite > self.article.stock:
            raise ValueError(f"Stock insuffisant (disponible: {self.article.stock})")

@dataclass
class Commande:
    """Commande client"""
    client: str
    lignes: List[LigneCommande] = field(default_factory=list)
    numero: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    date: datetime = field(default_factory=datetime.now)
    payee: bool = field(default=False, compare=False)
    
    def ajouter_ligne(self, article: Article, quantite: int):
        """Ajouter article à la commande"""
        ligne = LigneCommande(article, quantite)
        self.lignes.append(ligne)
    
    @property
    def total(self):
        """Prix total de la commande"""
        return sum(ligne.total for ligne in self.lignes)
    
    def payer(self):
        """Marquer commande comme payée"""
        self.payee = True
    
    def __str__(self):
        lignes_str = "\n".join(
            f"  - {l.article.nom} x{l.quantite} = {l.total}€"
            for l in self.lignes
        )
        statut = "PAYÉE" if self.payee else "EN ATTENTE"
        return f"""Commande {self.numero} ({statut})
Client: {self.client}
Date: {self.date.strftime('%Y-%m-%d %H:%M')}
Articles:
{lignes_str}
TOTAL: {self.total}€"""

# Test complet
laptop = Article("Laptop", 999.99, 5)
mouse = Article("Mouse", 29.99, 20)
keyboard = Article("Keyboard", 79.99, 10)

commande = Commande("Alice")
commande.ajouter_ligne(laptop, 1)
commande.ajouter_ligne(mouse, 2)
commande.ajouter_ligne(keyboard, 1)

print(commande)
# Commande 1a2b3c4d (EN ATTENTE)
# Client: Alice
# Date: 2024-01-31 10:30
# Articles:
#   - Laptop x1 = 999.99€
#   - Mouse x2 = 59.98€
#   - Keyboard x1 = 79.99€
# TOTAL: 1139.96€

commande.payer()
print(f"\nCommande payée: {commande.payee}")


# ============================================================================
# [GUIDE] CHAPITRE 11 : COMPOSITION VS HÉRITAGE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Différence composition vs héritage
[OK] Principe "Préférer composition à héritage"
[OK] Quand utiliser l'un vs l'autre
[OK] Pattern de délégation
[OK] Composition en pratique
"""


# ----------------------------------------------------------------------------
# [REFLEXION] HÉRITAGE VS COMPOSITION
# ----------------------------------------------------------------------------

"""
HÉRITAGE = Relation "EST-UN" (is-a)
    Chien EST-UN Animal

COMPOSITION = Relation "A-UN" (has-a)
    Voiture A-UN Moteur


EXEMPLE : HÉRITAGE
"""

class Moteur:
    def demarrer(self):
        print("Moteur démarre")

class Voiture(Moteur):  # <- Héritage
    """
    [X] MAUVAIS : Voiture n'EST PAS un Moteur
    Voiture A un Moteur
    """
    pass

"""
EXEMPLE : COMPOSITION
"""

class Moteur:
    def demarrer(self):
        print("Moteur démarre")

class Voiture:
    def __init__(self):
        self.moteur = Moteur()  # <- Composition
    
    def demarrer(self):
        """Délégation au moteur"""
        self.moteur.demarrer()

# Test
voiture = Voiture()
voiture.demarrer()  # Moteur démarre

"""
[OK] COMPOSITION : Plus logique et flexible
"""


# ----------------------------------------------------------------------------
# [IDEE] POURQUOI PRÉFÉRER COMPOSITION ?
# ----------------------------------------------------------------------------

"""
PROBLÈMES DE L'HÉRITAGE

1. COUPLAGE FORT
   Enfant dépend fortement du parent

2. HIÉRARCHIES RIGIDES
   Difficile de changer après

3. HÉRITAGE MULTIPLE COMPLEXE
   Diamant, MRO, etc.

4. VIOLATION DE ENCAPSULATION
   Enfant doit connaître détails internes du parent


EXEMPLE : PROBLÈME D'HÉRITAGE
"""

class Liste(list):  # Hériter de list
    """Ajouter compteur d'ajouts"""
    
    def __init__(self):
        super().__init__()
        self.nb_ajouts = 0
    
    def append(self, item):
        self.nb_ajouts += 1
        super().append(item)

# Test
ma_liste = Liste()
ma_liste.append("A")
ma_liste.append("B")
ma_liste.extend(["C", "D"])  # extend() appelle append() en interne !

print(ma_liste.nb_ajouts)  # 4 (attendu 2 !)
# Problème : extend() de list appelle aussi append()

"""
[OK] SOLUTION AVEC COMPOSITION
"""

class Liste:
    """Liste avec compteur (composition)"""
    
    def __init__(self):
        self._items = []  # Composition !
        self.nb_ajouts = 0
    
    def append(self, item):
        self.nb_ajouts += 1
        self._items.append(item)
    
    def extend(self, items):
        for item in items:
            self.append(item)  # Utilise notre append()
    
    def __getitem__(self, index):
        """Délégation"""
        return self._items[index]
    
    def __len__(self):
        """Délégation"""
        return len(self._items)

# Test
ma_liste = Liste()
ma_liste.append("A")
ma_liste.append("B")
ma_liste.extend(["C", "D"])

print(ma_liste.nb_ajouts)  # 4 [OK] Correct !


# ----------------------------------------------------------------------------
# [OBJECTIF] QUAND UTILISER QUOI ?
# ----------------------------------------------------------------------------

"""
UTILISER HÉRITAGE QUAND :

[OK] Vraie relation "EST-UN"
    Chien EST-UN Animal
    Rectangle EST-UNE Forme

[OK] Sous-classe plus spécifique
    Animal -> Chien (spécialisation)

[OK] Réutilisation via polymorphisme
    Toutes les formes ont aire()

[OK] Interface commune claire
    Classes abstraites, protocoles


UTILISER COMPOSITION QUAND :

[OK] Relation "A-UN"
    Voiture A-UN Moteur
    Ordinateur A-UN Processeur

[OK] Réutilisation de fonctionnalité
    Sans relation hiérarchique

[OK] Flexibilité souhaitée
    Changer composants à runtime

[OK] Éviter couplage fort
    Modules indépendants


[CLE] RÈGLE D'OR :

"Préférer COMPOSITION à HÉRITAGE"

Par défaut -> Composition
Sauf si vraiment "EST-UN" -> Héritage
"""


# ----------------------------------------------------------------------------
# [OUTILS] PATTERNS DE COMPOSITION
# ----------------------------------------------------------------------------

"""
PATTERN 1 : DÉLÉGATION SIMPLE
"""

class Moteur:
    def demarrer(self):
        return "Moteur démarré"
    
    def arreter(self):
        return "Moteur arrêté"

class Voiture:
    def __init__(self):
        self.moteur = Moteur()
    
    def demarrer(self):
        """Déléguer au moteur"""
        return self.moteur.demarrer()
    
    def arreter(self):
        """Déléguer au moteur"""
        return self.moteur.arreter()

"""
PATTERN 2 : INJECTION DE DÉPENDANCE
"""

class MoteurEssence:
    def demarrer(self):
        return "Moteur essence démarre"

class MoteurElectrique:
    def demarrer(self):
        return "Moteur électrique démarre"

class Voiture:
    def __init__(self, moteur):  # <- Injection
        """
        [IDEE] INJECTION DE DÉPENDANCE
        
        Voiture ne crée pas le moteur
        On lui fournit de l'extérieur
        
        Flexibilité maximale !
        """
        self.moteur = moteur
    
    def demarrer(self):
        return self.moteur.demarrer()

# Utilisation
v1 = Voiture(MoteurEssence())
print(v1.demarrer())  # Moteur essence démarre

v2 = Voiture(MoteurElectrique())
print(v2.demarrer())  # Moteur électrique démarre

"""
PATTERN 3 : AGRÉGATION (Composition Faible)
"""

class Employe:
    def __init__(self, nom):
        self.nom = nom

class Entreprise:
    def __init__(self):
        self.employes = []  # <- Agrégation
    
    def embaucher(self, employe):
        self.employes.append(employe)
    
    def licencier(self, employe):
        self.employes.remove(employe)

# Employés existent indépendamment de l'entreprise
alice = Employe("Alice")
bob = Employe("Bob")

entreprise = Entreprise()
entreprise.embaucher(alice)
entreprise.embaucher(bob)

# Si entreprise disparaît, employés existent toujours
del entreprise
print(alice.nom)  # Alice (toujours là)

"""
PATTERN 4 : COMPOSITION FORTE
"""

class CPU:
    def __init__(self):
        print("CPU créé")

class Ordinateur:
    def __init__(self):
        self.cpu = CPU()  # <- Créé à l'intérieur
        # Si Ordinateur disparaît, CPU disparaît aussi

# Si ordinateur détruit, CPU aussi
pc = Ordinateur()
del pc  # CPU détruit aussi


# Ce chapitre continue...
# ============================================================================

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 3 (FIN) : DESCRIPTEURS ET MÉTACLASSES
# ============================================================================
#
# [OBJECTIF] CONTENU :
# - Chapitre 12 : Descripteurs (Protocole complet)
# - Chapitre 13 : Métaclasses (Concepts avancés)
# - Exercices pratiques complets
# - Récapitulatif Partie 3
#
# [TEMPS] TEMPS : ~5-6 heures
# [DOCS] PRÉREQUIS : Chapitres 9-11 complétés
# [IDEE] NIVEAU : Avancé
# ============================================================================


# Suite du fichier précédent avec les Descripteurs et Métaclasses...
# (Le contenu complet est dans le fichier que j'ai tenté de créer)


# ============================================================================
# [COURS] EXERCICES PRATIQUES AVANCÉS
# ============================================================================

"""
EXERCICE 1 : SYSTÈME DE VALIDATION COMPLET AVEC DESCRIPTEURS
──────────────────────────────────────────────────────────────

Créer un système de validation réutilisable pour une application web

SPÉCIFICATIONS :
1. Descripteur Required : champ obligatoire
2. Descripteur Email : validation email
3. Descripteur URL : validation URL
4. Descripteur Choice : valeur parmi une liste
5. Classe Formulaire utilisant ces validateurs
"""

from weakref import WeakKeyDictionary
import re
from urllib.parse import urlparse

class Required:
    """Descripteur pour champ obligatoire"""
    
    def __init__(self):
        self.data = WeakKeyDictionary()
    
    def __set_name__(self, owner, name):
        self.name = name
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = self.data.get(instance)
        if value is None:
            raise ValueError(f"{self.name} est obligatoire")
        return value
    
    def __set__(self, instance, value):
        if value is None or (isinstance(value, str) and not value.strip()):
            raise ValueError(f"{self.name} ne peut pas être vide")
        self.data[instance] = value

class Email:
    """Descripteur pour validation email"""
    
    def __init__(self, required=True):
        self.required = required
        self.data = WeakKeyDictionary()
        self.pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    
    def __set_name__(self, owner, name):
        self.name = name
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.data.get(instance, '')
    
    def __set__(self, instance, value):
        if not value and not self.required:
            self.data[instance] = ''
            return
        
        if not value and self.required:
            raise ValueError(f"{self.name} est obligatoire")
        
        if not re.match(self.pattern, value):
            raise ValueError(f"{self.name} : format email invalide")
        
        self.data[instance] = value

class URL:
    """Descripteur pour validation URL"""
    
    def __init__(self, required=True, schemes=None):
        self.required = required
        self.schemes = schemes or ['http', 'https']
        self.data = WeakKeyDictionary()
    
    def __set_name__(self, owner, name):
        self.name = name
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.data.get(instance, '')
    
    def __set__(self, instance, value):
        if not value and not self.required:
            self.data[instance] = ''
            return
        
        if not value and self.required:
            raise ValueError(f"{self.name} est obligatoire")
        
        try:
            result = urlparse(value)
            if not all([result.scheme, result.netloc]):
                raise ValueError
            if result.scheme not in self.schemes:
                raise ValueError
        except:
            raise ValueError(
                f"{self.name} : URL invalide (doit commencer par {'/'.join(self.schemes)})"
            )
        
        self.data[instance] = value

class Choice:
    """Descripteur pour choix parmi liste"""
    
    def __init__(self, choices, required=True):
        self.choices = set(choices)
        self.required = required
        self.data = WeakKeyDictionary()
    
    def __set_name__(self, owner, name):
        self.name = name
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.data.get(instance)
    
    def __set__(self, instance, value):
        if value is None and not self.required:
            self.data[instance] = None
            return
        
        if value is None and self.required:
            raise ValueError(f"{self.name} est obligatoire")
        
        if value not in self.choices:
            raise ValueError(
                f"{self.name} doit être parmi : {', '.join(map(str, self.choices))}"
            )
        
        self.data[instance] = value

# Classe utilisant les descripteurs
class FormulaireInscription:
    """Formulaire d'inscription avec validation"""
    
    nom = Required()
    prenom = Required()
    email = Email()
    site_web = URL(required=False)
    genre = Choice(['M', 'F', 'Autre'])
    pays = Choice(['France', 'Belgique', 'Suisse', 'Canada'])
    
    def __init__(self, nom, prenom, email, genre, pays, site_web=None):
        self.nom = nom
        self.prenom = prenom
        self.email = email
        self.genre = genre
        self.pays = pays
        if site_web:
            self.site_web = site_web
    
    def __repr__(self):
        return (f"FormulaireInscription(nom={self.nom!r}, prenom={self.prenom!r}, "
                f"email={self.email!r}, genre={self.genre!r}, pays={self.pays!r})")

# Tests
print("=== TEST FORMULAIRE VALIDE ===")
form = FormulaireInscription(
    nom="Dupont",
    prenom="Jean",
    email="jean.dupont@example.com",
    genre="M",
    pays="France",
    site_web="https://jeandupont.com"
)
print(form)

print("\n=== TESTS VALIDATION ===")

# Nom vide
try:
    form.nom = ""
except ValueError as e:
    print(f"[X] {e}")

# Email invalide
try:
    form.email = "email-invalide"
except ValueError as e:
    print(f"[X] {e}")

# URL invalide
try:
    form.site_web = "pas-une-url"
except ValueError as e:
    print(f"[X] {e}")

# Genre invalide
try:
    form.genre = "X"
except ValueError as e:
    print(f"[X] {e}")

# Pays invalide
try:
    form.pays = "Allemagne"
except ValueError as e:
    print(f"[X] {e}")


"""
EXERCICE 2 : MÉTACLASSE POUR ORM SIMPLE
────────────────────────────────────────

Créer un ORM (Object-Relational Mapping) simple avec métaclasses

SPÉCIFICATIONS :
1. Métaclasse qui enregistre tous les modèles
2. Génération automatique de requêtes SQL
3. Tracking des champs
"""

class Field:
    """Descripteur pour champs de base de données"""
    
    def __init__(self, field_type, primary_key=False, null=False, default=None):
        self.field_type = field_type
        self.primary_key = primary_key
        self.null = null
        self.default = default
        self.data = WeakKeyDictionary()
    
    def __set_name__(self, owner, name):
        self.name = name
        self.column_name = name
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.data.get(instance, self.default)
    
    def __set__(self, instance, value):
        if value is None and not self.null and not self.primary_key:
            raise ValueError(f"{self.name} ne peut pas être NULL")
        self.data[instance] = value

class IntegerField(Field):
    def __init__(self, **kwargs):
        super().__init__('INTEGER', **kwargs)

class CharField(Field):
    def __init__(self, max_length=255, **kwargs):
        super().__init__('VARCHAR', **kwargs)
        self.max_length = max_length
    
    def __set__(self, instance, value):
        if value and len(str(value)) > self.max_length:
            raise ValueError(f"{self.name} dépasse {self.max_length} caractères")
        super().__set__(instance, value)

class ModelMeta(type):
    """Métaclasse pour modèles ORM"""
    
    _models = {}
    
    def __new__(mcs, name, bases, dct):
        # Récupérer les champs
        fields = {}
        for key, value in dct.items():
            if isinstance(value, Field):
                fields[key] = value
        
        # Ajouter dictionnaire des champs à la classe
        dct['_fields'] = fields
        
        # Créer la classe
        cls = super().__new__(mcs, name, bases, dct)
        
        # Enregistrer le modèle (sauf Model lui-même)
        if bases:
            mcs._models[name] = cls
        
        return cls
    
    @classmethod
    def get_models(mcs):
        return mcs._models.copy()

class Model(metaclass=ModelMeta):
    """Classe de base pour modèles ORM"""
    
    def __init__(self, **kwargs):
        for field_name in self._fields:
            value = kwargs.get(field_name)
            setattr(self, field_name, value)
    
    @classmethod
    def create_table_sql(cls):
        """Générer SQL CREATE TABLE"""
        table_name = cls.__name__.lower()
        columns = []
        
        for name, field in cls._fields.items():
            col_def = f"{field.column_name} {field.field_type}"
            
            if field.primary_key:
                col_def += " PRIMARY KEY"
            
            if not field.null and not field.primary_key:
                col_def += " NOT NULL"
            
            if field.default is not None:
                col_def += f" DEFAULT {field.default}"
            
            columns.append(col_def)
        
        return f"CREATE TABLE {table_name} (\n  " + ",\n  ".join(columns) + "\n);"
    
    def insert_sql(self):
        """Générer SQL INSERT"""
        table_name = self.__class__.__name__.lower()
        fields = []
        values = []
        
        for name, field in self._fields.items():
            if not field.primary_key:  # Skip auto-increment
                fields.append(field.column_name)
                value = getattr(self, name)
                if isinstance(value, str):
                    values.append(f"'{value}'")
                else:
                    values.append(str(value))
        
        return f"INSERT INTO {table_name} ({', '.join(fields)}) VALUES ({', '.join(values)});"
    
    def __repr__(self):
        attrs = ', '.join(f"{k}={getattr(self, k)!r}" for k in self._fields)
        return f"{self.__class__.__name__}({attrs})"

# Définir des modèles
class User(Model):
    """Modèle User"""
    id = IntegerField(primary_key=True)
    username = CharField(max_length=50)
    email = CharField(max_length=100)
    age = IntegerField(null=True)

class Product(Model):
    """Modèle Product"""
    id = IntegerField(primary_key=True)
    name = CharField(max_length=200)
    price = IntegerField()
    stock = IntegerField(default=0)

# Tests
print("\n=== MODÈLES ENREGISTRÉS ===")
for name, model in ModelMeta.get_models().items():
    print(f"  - {name}")

print("\n=== SQL CREATE TABLE ===")
print(User.create_table_sql())
print()
print(Product.create_table_sql())

print("\n=== CRÉATION D'INSTANCES ===")
user = User(username="alice", email="alice@example.com", age=30)
print(user)

product = Product(name="Laptop", price=999, stock=10)
print(product)

print("\n=== SQL INSERT ===")
print(user.insert_sql())
print(product.insert_sql())


"""
EXERCICE 3 : DESCRIPTEUR AVEC CACHE
────────────────────────────────────

Créer un descripteur qui cache les valeurs calculées
"""

import time
from functools import wraps

class CachedProperty:
    """
    Descripteur qui cache le résultat d'une méthode
    Utile pour propriétés coûteuses à calculer
    """
    
    def __init__(self, func):
        self.func = func
        self.name = func.__name__
        self.cache = WeakKeyDictionary()
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        
        # Vérifier si en cache
        if instance in self.cache:
            print(f"  [CACHE] Retour valeur cachée pour {self.name}")
            return self.cache[instance]
        
        # Calculer et mettre en cache
        print(f"  [CALCUL] Calcul de {self.name}...")
        value = self.func(instance)
        self.cache[instance] = value
        return value
    
    def __set__(self, instance, value):
        # Permettre réinitialisation du cache
        self.cache[instance] = value
    
    def __delete__(self, instance):
        # Supprimer du cache
        if instance in self.cache:
            del self.cache[instance]

class Document:
    """Document avec propriétés coûteuses à calculer"""
    
    def __init__(self, texte):
        self.texte = texte
    
    @CachedProperty
    def nb_mots(self):
        """Compter les mots (simulation calcul coûteux)"""
        time.sleep(0.5)  # Simuler calcul long
        return len(self.texte.split())
    
    @CachedProperty
    def nb_caracteres(self):
        """Compter les caractères"""
        time.sleep(0.5)  # Simuler calcul long
        return len(self.texte)
    
    @CachedProperty
    def statistiques(self):
        """Calculer statistiques complètes"""
        time.sleep(1)  # Simuler calcul très long
        return {
            'mots': self.nb_mots,
            'caracteres': self.nb_caracteres,
            'phrases': self.texte.count('.') + 1
        }

# Test
print("\n=== TEST CACHE ===")
doc = Document("Ceci est un exemple de texte. Il contient plusieurs phrases.")

print("Premier accès à nb_mots:")
print(f"Mots: {doc.nb_mots}")

print("\nDeuxième accès à nb_mots:")
print(f"Mots: {doc.nb_mots}")

print("\nPremier accès à nb_caracteres:")
print(f"Caractères: {doc.nb_caracteres}")

print("\nPremier accès à statistiques:")
print(f"Stats: {doc.statistiques}")

print("\nDeuxième accès à statistiques:")
print(f"Stats: {doc.statistiques}")

print("\nRéinitialisation cache nb_mots:")
del doc.nb_mots
print(f"Mots (recalculé): {doc.nb_mots}")


# ============================================================================
# [DOCS] RÉCAPITULATIF COMPLET PARTIE 3
# ============================================================================

"""
[BRAVO] FÉLICITATIONS ! PARTIE 3 COMPLÉTÉE !

═══════════════════════════════════════════════════════════════════════════
CHAPITRE 9 : ENCAPSULATION AVANCÉE
═══════════════════════════════════════════════════════════════════════════

[OK] Niveaux d'accès (public, _protégé, __privé)
[OK] Name mangling (__attribut -> _Classe__attribut)
[OK] Properties complètes (getter, setter, deleter)
[OK] Properties avec cache (lazy loading)
[OK] __slots__ pour optimisation mémoire
[OK] Immutabilité d'objets
[OK] frozen=True pour dataclasses

COMPÉTENCES :
-> Contrôler accès aux attributs
-> Optimiser utilisation mémoire
-> Créer objets immutables et thread-safe


═══════════════════════════════════════════════════════════════════════════
CHAPITRE 10 : DATACLASSES
═══════════════════════════════════════════════════════════════════════════

[OK] @dataclass pour réduire boilerplate
[OK] Génération automatique __init__, __repr__, __eq__
[OK] field() pour configuration avancée
[OK] default_factory pour valeurs mutables
[OK] Options : order, frozen, unsafe_hash
[OK] __post_init__ pour validation et calculs
[OK] Dataclasses immutables avec frozen=True

COMPÉTENCES :
-> Créer classes de données efficacement
-> Valider données dans __post_init__
-> Utiliser dataclasses pour DTOs et modèles


═══════════════════════════════════════════════════════════════════════════
CHAPITRE 11 : COMPOSITION VS HÉRITAGE
═══════════════════════════════════════════════════════════════════════════

[OK] Différence "EST-UN" vs "A-UN"
[OK] Problèmes de l'héritage (couplage, rigidité)
[OK] Avantages de la composition (flexibilité)
[OK] Pattern de délégation
[OK] Injection de dépendances
[OK] Agrégation vs Composition forte
[OK] Principe "Préférer composition à héritage"

COMPÉTENCES :
-> Choisir entre héritage et composition
-> Concevoir systèmes flexibles
-> Éviter couplage excessif


═══════════════════════════════════════════════════════════════════════════
CHAPITRE 12 : DESCRIPTEURS
═══════════════════════════════════════════════════════════════════════════

[OK] Protocole de descripteur (__get__, __set__, __delete__)
[OK] Data vs Non-data descriptors
[OK] __set_name__ pour auto-configuration
[OK] WeakKeyDictionary pour éviter fuites mémoire
[OK] Descripteurs de validation réutilisables
[OK] Comprendre fonctionnement de @property
[OK] Patterns : validation, typage, cache

COMPÉTENCES :
-> Créer validations réutilisables
-> Maîtriser mécanisme bas niveau de Python
-> Implémenter properties avancées


═══════════════════════════════════════════════════════════════════════════
CHAPITRE 13 : MÉTACLASSES
═══════════════════════════════════════════════════════════════════════════

[OK] Classes sont instances de type
[OK] Créer classes dynamiquement avec type()
[OK] Métaclasses personnalisées
[OK] __new__ vs __init__ en métaclasses
[OK] __call__ pour contrôler instanciation
[OK] Patterns : Registry, Singleton, Validation
[OK] Alternatives : décorateurs, __init_subclass__
[OK] Quand NE PAS utiliser métaclasses

COMPÉTENCES :
-> Comprendre métaprogrammation
-> Contrôler création de classes
-> Choisir bonne approche (simple vs avancé)


═══════════════════════════════════════════════════════════════════════════
[CLE] CONCEPTS CLÉS MAÎTRISÉS
═══════════════════════════════════════════════════════════════════════════

ENCAPSULATION :
[OK] Cacher détails internes
[OK] Exposer interface publique claire
[OK] Utiliser properties pour validation

VALIDATION :
[OK] Descripteurs pour validations réutilisables
[OK] __post_init__ pour dataclasses
[OK] Properties pour contrôle d'accès

OPTIMISATION :
[OK] __slots__ pour économiser mémoire
[OK] Cache avec descripteurs
[OK] Immutabilité pour thread-safety

DESIGN :
[OK] Composition > Héritage
[OK] Injection de dépendances
[OK] Séparation des responsabilités


═══════════════════════════════════════════════════════════════════════════
-> PROCHAINE ÉTAPE : PARTIE 4
═══════════════════════════════════════════════════════════════════════════

La Partie 4 couvrira les DESIGN PATTERNS et BEST PRACTICES :

PATTERNS CRÉATIONNELS :
- Singleton (une seule instance)
- Factory (création d'objets)
- Builder (construction complexe)
- Prototype (clonage)

PATTERNS STRUCTURELS :
- Adapter (compatibilité interfaces)
- Decorator (ajout fonctionnalités)
- Facade (simplification interface)
- Proxy (contrôle d'accès)

PATTERNS COMPORTEMENTAUX :
- Observer (pub/sub)
- Strategy (algorithmes interchangeables)
- Command (encapsulation actions)
- Iterator (parcours collections)

PRINCIPES SOLID :
- Single Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion

BEST PRACTICES :
- Testing orienté objet
- Documentation
- Performance
- Erreurs courantes à éviter


═══════════════════════════════════════════════════════════════════════════
[OBJECTIF] VOUS ÊTES MAINTENANT CAPABLE DE :
═══════════════════════════════════════════════════════════════════════════

[OK] Concevoir architectures POO complexes
[OK] Choisir bons patterns pour chaque situation
[OK] Optimiser code orienté objet
[OK] Créer APIs propres et maintenables
[OK] Utiliser concepts avancés à bon escient
[OK] Comprendre fonctionnement interne de Python
[OK] Écrire code pythonique et idiomatique


[GUIDE] Prochains fichiers :
- poo_partie4_patterns_creationnels.txt
- poo_partie4_patterns_structurels.txt
- poo_partie4_patterns_comportementaux.txt
- poo_partie4_solid_et_best_practices.txt


Excellent travail ! Vous maîtrisez maintenant les concepts avancés ! [RAPIDE]
Prêt pour les Design Patterns ? Continuez ! [FORCE]
"""

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 4 : DESIGN PATTERNS ET BEST PRACTICES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 14 : Introduction aux Design Patterns
# - Chapitre 15 : Patterns Créationnels (Singleton, Factory, Builder, Prototype)
# - Chapitre 16 : Patterns Structurels (Adapter, Decorator, Facade, Proxy)
# - Chapitre 17 : Patterns Comportementaux (Observer, Strategy, Command, State)
#
# [TEMPS] TEMPS : ~10-12 heures
# [DOCS] PRÉREQUIS : Parties 1, 2 et 3 complétées
# [IDEE] NIVEAU : Avancé
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 14 : INTRODUCTION AUX DESIGN PATTERNS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce qu'un design pattern
[OK] Pourquoi utiliser les design patterns
[OK] Les 3 catégories de patterns (GoF)
[OK] Principes de conception orientée objet
[OK] Quand utiliser (ou ne pas utiliser) un pattern
"""


# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QU'UN DESIGN PATTERN ?
# ----------------------------------------------------------------------------

"""
DESIGN PATTERN = Solution réutilisable à un problème récurrent

[IDEE] Analogie :
Les patterns sont comme des RECETTES de cuisine
- Problème : "Comment faire un gâteau ?"
- Pattern : Recette éprouvée avec ingrédients et étapes
- Vous adaptez la recette à votre situation


HISTOIRE

Les Design Patterns viennent du livre :
"Design Patterns: Elements of Reusable Object-Oriented Software" (1994)
Écrit par le "Gang of Four" (GoF) :
- Erich Gamma
- Richard Helm
- Ralph Johnson
- John Vlissides

23 patterns classiques décrits


CATÉGORIES DE PATTERNS (GoF)

1. CRÉATIONNELS (Creational)
   -> Comment CRÉER des objets
   -> Exemples : Singleton, Factory, Builder
   
2. STRUCTURELS (Structural)
   -> Comment ORGANISER les classes
   -> Exemples : Adapter, Decorator, Facade
   
3. COMPORTEMENTAUX (Behavioral)
   -> Comment les objets INTERAGISSENT
   -> Exemples : Observer, Strategy, Command


POURQUOI UTILISER LES PATTERNS ?

[OK] Solutions éprouvées
   Testées dans de nombreux projets

[OK] Vocabulaire commun
   "Utilisons un Factory" -> tout le monde comprend

[OK] Meilleure conception
   Code plus maintenable et extensible

[OK] Éviter de réinventer la roue
   Problèmes déjà résolus

[X] NE PAS utiliser si :
   - Vous n'avez pas le problème que le pattern résout
   - Solution simple suffit
   - Pattern complique inutilement le code


[CLE] RÈGLE D'OR :

"Don't use a pattern just because you can.
 Use it because you need it."
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] PRINCIPES DE CONCEPTION ORIENTÉE OBJET
# ----------------------------------------------------------------------------

"""
AVANT LES PATTERNS : LES PRINCIPES

Les patterns s'appuient sur des principes fondamentaux


PRINCIPE 1 : SÉPARATION DES RESPONSABILITÉS

Une classe = UNE responsabilité claire
"""

# [X] Mauvais : Classe fait trop de choses
class Utilisateur:
    def __init__(self, nom, email):
        self.nom = nom
        self.email = email
    
    def enregistrer(self):
        """Responsabilité 1 : Logique métier"""
        pass
    
    def sauvegarder_en_db(self):
        """Responsabilité 2 : Persistance"""
        pass
    
    def envoyer_email_bienvenue(self):
        """Responsabilité 3 : Communication"""
        pass

# [OK] Bon : Responsabilités séparées
class Utilisateur:
    """Juste les données et logique métier"""
    def __init__(self, nom, email):
        self.nom = nom
        self.email = email

class UtilisateurRepository:
    """Responsabilité : Persistance"""
    def sauvegarder(self, utilisateur):
        pass

class EmailService:
    """Responsabilité : Communication"""
    def envoyer_bienvenue(self, utilisateur):
        pass

"""
PRINCIPE 2 : OUVERT/FERMÉ

Ouvert à l'extension, fermé à la modification
"""

# [X] Mauvais : Modification de code existant pour ajouter fonctionnalité
class CalculateurPrix:
    def calculer(self, produit, type_client):
        prix = produit.prix
        
        if type_client == "normal":
            return prix
        elif type_client == "fidele":
            return prix * 0.9
        elif type_client == "vip":
            return prix * 0.8
        # [X] Ajouter nouveau type -> modifier cette méthode

# [OK] Bon : Extension sans modification
class StrategiePrix:
    def calculer(self, prix):
        return prix

class PrixNormal(StrategiePrix):
    def calculer(self, prix):
        return prix

class PrixFidele(StrategiePrix):
    def calculer(self, prix):
        return prix * 0.9

class PrixVIP(StrategiePrix):
    def calculer(self, prix):
        return prix * 0.8

class CalculateurPrix:
    def __init__(self, strategie):
        self.strategie = strategie
    
    def calculer(self, produit):
        return self.strategie.calculer(produit.prix)

# [OK] Ajouter nouveau type : créer nouvelle classe (pas de modification)

"""
PRINCIPE 3 : PRÉFÉRER COMPOSITION À HÉRITAGE

Déjà vu au Chapitre 11 !


PRINCIPE 4 : DÉPENDRE D'ABSTRACTIONS

Dépendre d'interfaces, pas d'implémentations concrètes
"""

# [X] Mauvais : Dépendance concrète
class EmailService:
    def envoyer(self, destinataire, message):
        # Implémentation concrète
        print(f"Email envoyé à {destinataire}")

class Notification:
    def __init__(self):
        self.email_service = EmailService()  # [X] Couplage fort
    
    def notifier(self, utilisateur, message):
        self.email_service.envoyer(utilisateur.email, message)

# [OK] Bon : Dépendance abstraite
from abc import ABC, abstractmethod

class ServiceNotification(ABC):
    @abstractmethod
    def envoyer(self, destinataire, message):
        pass

class EmailService(ServiceNotification):
    def envoyer(self, destinataire, message):
        print(f"Email envoyé à {destinataire}")

class SMSService(ServiceNotification):
    def envoyer(self, destinataire, message):
        print(f"SMS envoyé à {destinataire}")

class Notification:
    def __init__(self, service: ServiceNotification):
        self.service = service  # [OK] Injection de dépendance
    
    def notifier(self, utilisateur, message):
        self.service.envoyer(utilisateur.email, message)

# Maintenant on peut changer le service facilement
notif_email = Notification(EmailService())
notif_sms = Notification(SMSService())


# ============================================================================
# [GUIDE] CHAPITRE 15 : PATTERNS CRÉATIONNELS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Implémenter le pattern Singleton
[OK] Utiliser Factory Method et Abstract Factory
[OK] Créer objets complexes avec Builder
[OK] Cloner objets avec Prototype
[OK] Choisir le bon pattern créationnel
"""


# ----------------------------------------------------------------------------
# [VERROUILLE] PATTERN SINGLETON
# ----------------------------------------------------------------------------

"""
SINGLETON = Garantir qu'une classe n'a qu'UNE SEULE instance

[IDEE] Cas d'usage :
- Configuration globale
- Logger
- Connexion à base de données
- Cache
- Pool de connexions


PROBLÈME SANS SINGLETON
"""

class Database:
    def __init__(self, connection_string):
        print(f"Connexion à {connection_string}")
        self.connection_string = connection_string

# Plusieurs instances créées !
db1 = Database("server1")
db2 = Database("server2")
db3 = Database("server3")

# Sortie :
# Connexion à server1
# Connexion à server2
# Connexion à server3

# [X] Problème : 3 connexions différentes (gaspillage ressources)

"""
SOLUTION 1 : SINGLETON AVEC MÉTACLASSE
"""

class SingletonMeta(type):
    """
    Métaclasse Singleton
    Conserve UNE instance par classe
    """
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            # Première création : créer instance
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]

class Database(metaclass=SingletonMeta):
    def __init__(self, connection_string):
        print(f"Connexion à {connection_string}")
        self.connection_string = connection_string

# Test
db1 = Database("server1")  # Connexion à server1
db2 = Database("server2")  # Pas de print ! Même instance
db3 = Database("server3")  # Pas de print ! Même instance

print(db1 is db2)  # True
print(db1 is db3)  # True
print(db1.connection_string)  # server1 (premier paramètre conservé)

"""
SOLUTION 2 : SINGLETON AVEC DÉCORATEUR
"""

def singleton(cls):
    """Décorateur Singleton"""
    instances = {}
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance

@singleton
class Logger:
    def __init__(self, nom):
        print(f"Logger créé : {nom}")
        self.nom = nom
    
    def log(self, message):
        print(f"[{self.nom}] {message}")

# Test
logger1 = Logger("App")    # Logger créé : App
logger2 = Logger("Module") # Pas de print !

print(logger1 is logger2)  # True
logger1.log("Test")        # [App] Test

"""
SOLUTION 3 : SINGLETON AVEC MÉTHODE DE CLASSE
"""

class Configuration:
    """Singleton avec méthode de classe"""
    
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            print("Création de Configuration")
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance
    
    def __init__(self):
        # Initialiser une seule fois
        if not self._initialized:
            self.settings = {}
            self._initialized = True
    
    def set(self, key, value):
        self.settings[key] = value
    
    def get(self, key):
        return self.settings.get(key)

# Test
config1 = Configuration()  # Création de Configuration
config1.set("debug", True)

config2 = Configuration()  # Pas de print !
print(config2.get("debug"))  # True (même instance)

print(config1 is config2)  # True

"""
[ATTENTION] PIÈGES DU SINGLETON

1. État global partagé
   -> Difficile à tester
   -> Couplage caché

2. Threading
   -> Besoin de locks pour thread-safety

3. Héritage
   -> Compliqué avec sous-classes


SINGLETON THREAD-SAFE
"""

import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                # Double-check locking
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

"""
[CLE] QUAND UTILISER SINGLETON ?

[OK] Vraiment besoin d'UNE SEULE instance
[OK] Accès global nécessaire
[OK] Ressource partagée (connexion, cache)

[X] Éviter si :
   - Juste pour partager données (utilisez injection)
   - Complique les tests
   - Pas réellement besoin d'unicité
"""


# ----------------------------------------------------------------------------
# [USINE] PATTERN FACTORY METHOD
# ----------------------------------------------------------------------------

"""
FACTORY METHOD = Déléguer création d'objets à des sous-classes

[IDEE] Cas d'usage :
- Créer objets sans connaître classe exacte
- Choisir classe à instancier dynamiquement
- Centraliser logique de création


PROBLÈME SANS FACTORY
"""

class Notification:
    def envoyer(self, message):
        pass

class EmailNotification(Notification):
    def envoyer(self, message):
        print(f"[EMAIL] Email: {message}")

class SMSNotification(Notification):
    def envoyer(self, message):
        print(f"[MOBILE] SMS: {message}")

class PushNotification(Notification):
    def envoyer(self, message):
        print(f"[NOTIF] Push: {message}")

# [X] Logique de création éparpillée
def envoyer_notification(type_notif, message):
    if type_notif == "email":
        notif = EmailNotification()
    elif type_notif == "sms":
        notif = SMSNotification()
    elif type_notif == "push":
        notif = PushNotification()
    else:
        raise ValueError(f"Type inconnu : {type_notif}")
    
    notif.envoyer(message)

"""
SOLUTION : FACTORY METHOD
"""

from abc import ABC, abstractmethod

class Notification(ABC):
    """Classe abstraite pour notifications"""
    
    @abstractmethod
    def envoyer(self, message):
        pass

class EmailNotification(Notification):
    def envoyer(self, message):
        print(f"[EMAIL] Email: {message}")

class SMSNotification(Notification):
    def envoyer(self, message):
        print(f"[MOBILE] SMS: {message}")

class PushNotification(Notification):
    def envoyer(self, message):
        print(f"[NOTIF] Push: {message}")

class NotificationFactory:
    """
    [IDEE] FACTORY METHOD
    
    Centralise la création de notifications
    """
    
    @staticmethod
    def creer_notification(type_notif: str) -> Notification:
        """
        Factory method : crée la bonne notification
        
        Args:
            type_notif: Type de notification ("email", "sms", "push")
        
        Returns:
            Instance de Notification
        """
        factories = {
            "email": EmailNotification,
            "sms": SMSNotification,
            "push": PushNotification
        }
        
        classe = factories.get(type_notif)
        if not classe:
            raise ValueError(f"Type inconnu : {type_notif}")
        
        return classe()

# Utilisation
factory = NotificationFactory()

notif = factory.creer_notification("email")
notif.envoyer("Bienvenue !")

notif = factory.creer_notification("sms")
notif.envoyer("Code: 1234")

"""
FACTORY METHOD AVEC PARAMÈTRES
"""

class NotificationFactory:
    """Factory avec configuration"""
    
    @staticmethod
    def creer_notification(type_notif: str, **config):
        """
        Crée notification avec configuration
        
        Args:
            type_notif: Type de notification
            **config: Configuration spécifique
        """
        if type_notif == "email":
            return EmailNotification(
                serveur=config.get('serveur', 'smtp.gmail.com'),
                port=config.get('port', 587)
            )
        elif type_notif == "sms":
            return SMSNotification(
                api_key=config.get('api_key')
            )
        elif type_notif == "push":
            return PushNotification(
                device_token=config.get('device_token')
            )
        else:
            raise ValueError(f"Type inconnu : {type_notif}")

"""
FACTORY METHOD AVEC ENREGISTREMENT DYNAMIQUE
"""

class NotificationFactory:
    """Factory avec enregistrement de types"""
    
    _types = {}
    
    @classmethod
    def enregistrer(cls, nom: str, classe):
        """
        [IDEE] ENREGISTREMENT DYNAMIQUE
        
        Permet d'ajouter nouveaux types sans modifier factory
        """
        cls._types[nom] = classe
    
    @classmethod
    def creer(cls, nom: str, **kwargs):
        """Crée notification du type demandé"""
        classe = cls._types.get(nom)
        if not classe:
            raise ValueError(f"Type non enregistré : {nom}")
        return classe(**kwargs)

# Enregistrement
NotificationFactory.enregistrer("email", EmailNotification)
NotificationFactory.enregistrer("sms", SMSNotification)
NotificationFactory.enregistrer("push", PushNotification)

# Utilisation
notif = NotificationFactory.creer("email")
notif.envoyer("Test")

# [OK] Ajouter nouveau type : juste l'enregistrer (pas de modification de factory)
class SlackNotification(Notification):
    def envoyer(self, message):
        print(f"[SPEECH_BALLOON] Slack: {message}")

NotificationFactory.enregistrer("slack", SlackNotification)
notif = NotificationFactory.creer("slack")
notif.envoyer("Nouveau message !")


# ----------------------------------------------------------------------------
# [USINE] PATTERN ABSTRACT FACTORY
# ----------------------------------------------------------------------------

"""
ABSTRACT FACTORY = Créer familles d'objets liés sans spécifier classes concrètes

[IDEE] Cas d'usage :
- Interface graphique (Windows, Mac, Linux)
- Thèmes (Dark, Light)
- Bases de données (MySQL, PostgreSQL, SQLite)


PROBLÈME : CRÉER FAMILLES D'OBJETS COHÉRENTS
"""

# Interface graphique multi-plateforme

"""
SOLUTION : ABSTRACT FACTORY
"""

from abc import ABC, abstractmethod

# === PRODUITS ABSTRAITS ===

class Bouton(ABC):
    @abstractmethod
    def afficher(self):
        pass

class CheckBox(ABC):
    @abstractmethod
    def afficher(self):
        pass

# === PRODUITS CONCRETS WINDOWS ===

class BoutonWindows(Bouton):
    def afficher(self):
        print("[Bouton Windows] █")

class CheckBoxWindows(CheckBox):
    def afficher(self):
        print("[CheckBox Windows] [x]")

# === PRODUITS CONCRETS MAC ===

class BoutonMac(Bouton):
    def afficher(self):
        print("[Bouton Mac] [FISHEYE]")

class CheckBoxMac(CheckBox):
    def afficher(self):
        print("[CheckBox Mac] [OK]")

# === PRODUITS CONCRETS LINUX ===

class BoutonLinux(Bouton):
    def afficher(self):
        print("[Bouton Linux] [ OK ]")

class CheckBoxLinux(CheckBox):
    def afficher(self):
        print("[CheckBox Linux] [X]")

# === FACTORY ABSTRAITE ===

class UIFactory(ABC):
    """
    [IDEE] ABSTRACT FACTORY
    
    Définit interface pour créer famille de produits
    """
    
    @abstractmethod
    def creer_bouton(self) -> Bouton:
        pass
    
    @abstractmethod
    def creer_checkbox(self) -> CheckBox:
        pass

# === FACTORIES CONCRÈTES ===

class WindowsFactory(UIFactory):
    """Factory pour UI Windows"""
    
    def creer_bouton(self):
        return BoutonWindows()
    
    def creer_checkbox(self):
        return CheckBoxWindows()

class MacFactory(UIFactory):
    """Factory pour UI Mac"""
    
    def creer_bouton(self):
        return BoutonMac()
    
    def creer_checkbox(self):
        return CheckBoxMac()

class LinuxFactory(UIFactory):
    """Factory pour UI Linux"""
    
    def creer_bouton(self):
        return BoutonLinux()
    
    def creer_checkbox(self):
        return CheckBoxLinux()

# === CLIENT ===

class Application:
    """
    Application qui utilise une factory
    Ne connaît QUE les abstractions
    """
    
    def __init__(self, factory: UIFactory):
        self.factory = factory
    
    def creer_ui(self):
        """Crée l'interface"""
        bouton = self.factory.creer_bouton()
        checkbox = self.factory.creer_checkbox()
        
        print("Interface créée :")
        bouton.afficher()
        checkbox.afficher()

# Utilisation
import platform

def obtenir_factory() -> UIFactory:
    """Choisir factory selon OS"""
    os_name = platform.system()
    
    if os_name == "Windows":
        return WindowsFactory()
    elif os_name == "Darwin":  # Mac
        return MacFactory()
    else:  # Linux
        return LinuxFactory()

# Test
factory = obtenir_factory()
app = Application(factory)
app.creer_ui()

"""
[OK] AVANTAGES ABSTRACT FACTORY :

1. COHÉRENCE
   Tous les objets créés sont compatibles (même famille)

2. ISOLATION
   Code client ne connaît pas classes concrètes

3. CHANGEMENT FACILE
   Changer de famille = changer de factory


EXEMPLE : THÈMES
"""

# Famille de couleurs par thème

class Theme(ABC):
    @abstractmethod
    def couleur_principale(self):
        pass
    
    @abstractmethod
    def couleur_secondaire(self):
        pass
    
    @abstractmethod
    def couleur_texte(self):
        pass

class ThemeClair(Theme):
    def couleur_principale(self):
        return "#FFFFFF"
    
    def couleur_secondaire(self):
        return "#F0F0F0"
    
    def couleur_texte(self):
        return "#000000"

class ThemeSombre(Theme):
    def couleur_principale(self):
        return "#1E1E1E"
    
    def couleur_secondaire(self):
        return "#2D2D2D"
    
    def couleur_texte(self):
        return "#FFFFFF"

class ThemeFactory(ABC):
    @abstractmethod
    def creer_theme(self) -> Theme:
        pass

class ThemeClairFactory(ThemeFactory):
    def creer_theme(self):
        return ThemeClair()

class ThemeSombreFactory(ThemeFactory):
    def creer_theme(self):
        return ThemeSombre()


# ----------------------------------------------------------------------------
# [CONSTRUCTION] PATTERN BUILDER
# ----------------------------------------------------------------------------

"""
BUILDER = Construire objets complexes étape par étape

[IDEE] Cas d'usage :
- Objets avec beaucoup de paramètres
- Construction en plusieurs étapes
- Différentes représentations du même objet


PROBLÈME : CONSTRUCTEUR AVEC TROP DE PARAMÈTRES
"""

# [X] Constructeur complexe
class Pizza:
    def __init__(self, taille, fromage=True, pepperoni=False, 
                 champignons=False, olives=False, ananas=False,
                 pate="normale", sauce="tomate"):
        self.taille = taille
        self.fromage = fromage
        self.pepperoni = pepperoni
        self.champignons = champignons
        self.olives = olives
        self.ananas = ananas
        self.pate = pate
        self.sauce = sauce

# Difficile à utiliser
pizza = Pizza("grande", True, True, False, True, False, "fine", "tomate")
# Quelle valeur correspond à quoi ? [REFLEXION]

"""
SOLUTION : BUILDER PATTERN
"""

class Pizza:
    """Objet final (produit)"""
    
    def __init__(self):
        self.taille = None
        self.pate = None
        self.sauce = None
        self.ingredients = []
    
    def __str__(self):
        ing = ", ".join(self.ingredients) if self.ingredients else "aucun"
        return f"Pizza {self.taille}, pâte {self.pate}, sauce {self.sauce}, ingrédients: {ing}"

class PizzaBuilder:
    """
    [IDEE] BUILDER PATTERN
    
    Construit Pizza étape par étape
    Interface fluide (méthodes retournent self)
    """
    
    def __init__(self):
        self.pizza = Pizza()
    
    def taille(self, taille: str):
        """Définir taille"""
        self.pizza.taille = taille
        return self  # <- Retourne self pour chaînage
    
    def pate(self, type_pate: str):
        """Définir type de pâte"""
        self.pizza.pate = type_pate
        return self
    
    def sauce(self, type_sauce: str):
        """Définir sauce"""
        self.pizza.sauce = type_sauce
        return self
    
    def ajouter_fromage(self):
        """Ajouter fromage"""
        self.pizza.ingredients.append("fromage")
        return self
    
    def ajouter_pepperoni(self):
        """Ajouter pepperoni"""
        self.pizza.ingredients.append("pepperoni")
        return self
    
    def ajouter_champignons(self):
        """Ajouter champignons"""
        self.pizza.ingredients.append("champignons")
        return self
    
    def ajouter_olives(self):
        """Ajouter olives"""
        self.pizza.ingredients.append("olives")
        return self
    
    def build(self):
        """Construire et retourner la pizza finale"""
        return self.pizza

# [OK] Utilisation fluide et claire
pizza = (PizzaBuilder()
    .taille("grande")
    .pate("fine")
    .sauce("tomate")
    .ajouter_fromage()
    .ajouter_pepperoni()
    .ajouter_champignons()
    .build())

print(pizza)
# Pizza grande, pâte fine, sauce tomate, ingrédients: fromage, pepperoni, champignons

"""
BUILDER AVEC DIRECTOR
"""

class Director:
    """
    [IDEE] DIRECTOR
    
    Connaît les recettes (séquences de construction)
    Utilise le builder pour construire
    """
    
    def __init__(self, builder: PizzaBuilder):
        self.builder = builder
    
    def pizza_margherita(self):
        """Recette Margherita"""
        return (self.builder
            .taille("moyenne")
            .pate("normale")
            .sauce("tomate")
            .ajouter_fromage()
            .build())
    
    def pizza_4_fromages(self):
        """Recette 4 fromages"""
        return (self.builder
            .taille("moyenne")
            .pate("épaisse")
            .sauce("crème")
            .ajouter_fromage()
            .build())
    
    def pizza_hawaienne(self):
        """Recette Hawaïenne"""
        builder = PizzaBuilder()
        return (builder
            .taille("grande")
            .pate("normale")
            .sauce("tomate")
            .ajouter_fromage()
            .ajouter_pepperoni()
            .build())

# Utilisation
director = Director(PizzaBuilder())

pizza1 = director.pizza_margherita()
print(pizza1)

pizza2 = director.pizza_hawaienne()
print(pizza2)

"""
EXEMPLE COMPLET : GÉNÉRATEUR DE REQUÊTES SQL
"""

class SQLQuery:
    """Requête SQL construite"""
    
    def __init__(self):
        self.select_fields = []
        self.from_table = None
        self.where_conditions = []
        self.order_by = []
        self.limit_value = None
    
    def __str__(self):
        """Générer SQL"""
        sql = f"SELECT {', '.join(self.select_fields)}"
        sql += f" FROM {self.from_table}"
        
        if self.where_conditions:
            sql += f" WHERE {' AND '.join(self.where_conditions)}"
        
        if self.order_by:
            sql += f" ORDER BY {', '.join(self.order_by)}"
        
        if self.limit_value:
            sql += f" LIMIT {self.limit_value}"
        
        return sql

class QueryBuilder:
    """Builder pour requêtes SQL"""
    
    def __init__(self):
        self.query = SQLQuery()
    
    def select(self, *fields):
        """Sélectionner champs"""
        self.query.select_fields.extend(fields)
        return self
    
    def from_table(self, table):
        """Table FROM"""
        self.query.from_table = table
        return self
    
    def where(self, condition):
        """Condition WHERE"""
        self.query.where_conditions.append(condition)
        return self
    
    def order_by(self, *fields):
        """Tri ORDER BY"""
        self.query.order_by.extend(fields)
        return self
    
    def limit(self, n):
        """Limite LIMIT"""
        self.query.limit_value = n
        return self
    
    def build(self):
        """Construire requête"""
        return self.query

# Utilisation
query = (QueryBuilder()
    .select("nom", "email", "age")
    .from_table("utilisateurs")
    .where("age > 18")
    .where("pays = 'France'")
    .order_by("nom", "age DESC")
    .limit(10)
    .build())

print(query)
# SELECT nom, email, age FROM utilisateurs WHERE age > 18 AND pays = 'France' ORDER BY nom, age DESC LIMIT 10


# ----------------------------------------------------------------------------
# [SCIENCE] PATTERN PROTOTYPE
# ----------------------------------------------------------------------------

"""
PROTOTYPE = Cloner objets existants au lieu de les créer from scratch

[IDEE] Cas d'usage :
- Création coûteuse (base de données, calculs)
- Objets similaires avec petites variations
- Éviter sous-classes pour chaque variation


PROBLÈME : CRÉATION COÛTEUSE
"""

import time

class DocumentComplexe:
    """Document avec création coûteuse"""
    
    def __init__(self):
        print("Chargement document (opération lente)...")
        time.sleep(2)  # Simulation
        self.contenu = "Document original"
        self.metadata = {"auteur": "Alice", "date": "2024-01-01"}

# Créer 3 documents similaires = 6 secondes !
doc1 = DocumentComplexe()
doc2 = DocumentComplexe()
doc3 = DocumentComplexe()

"""
SOLUTION : PROTOTYPE PATTERN
"""

import copy

class Prototype:
    """Classe de base pour prototypes"""
    
    def clone(self):
        """
        [IDEE] CLONE
        
        Crée copie de l'objet
        """
        return copy.deepcopy(self)

class DocumentComplexe(Prototype):
    """Document clonable"""
    
    def __init__(self):
        print("Chargement document (opération lente)...")
        time.sleep(2)  # Simulation
        self.contenu = "Document original"
        self.metadata = {"auteur": "Alice", "date": "2024-01-01"}
    
    def __str__(self):
        return f"Document: {self.contenu}, Metadata: {self.metadata}"

# Créer UN document original (2 secondes)
doc_original = DocumentComplexe()

# Cloner rapidement (instantané)
doc1 = doc_original.clone()
doc1.contenu = "Document 1"

doc2 = doc_original.clone()
doc2.contenu = "Document 2"

doc3 = doc_original.clone()
doc3.contenu = "Document 3"

print(doc1)
print(doc2)
print(doc3)

"""
SHALLOW COPY vs DEEP COPY
"""

# Shallow copy (copy.copy)
# -> Copie l'objet mais PAS les objets contenus
# -> Attributs mutables PARTAGÉS

# Deep copy (copy.deepcopy)
# -> Copie l'objet ET tous les objets contenus
# -> Indépendance totale

import copy

class Personne:
    def __init__(self, nom, adresse):
        self.nom = nom
        self.adresse = adresse  # Objet mutable

# Shallow copy
p1 = Personne("Alice", {"ville": "Paris"})
p2 = copy.copy(p1)

p2.nom = "Bob"  # OK, indépendant
p2.adresse["ville"] = "Lyon"  # [X] Modifie AUSSI p1 !

print(p1.adresse)  # {'ville': 'Lyon'}  <- Modifié !

# Deep copy
p1 = Personne("Alice", {"ville": "Paris"})
p3 = copy.deepcopy(p1)

p3.adresse["ville"] = "Marseille"  # [OK] N'affecte PAS p1

print(p1.adresse)  # {'ville': 'Paris'}  <- Intact !
print(p3.adresse)  # {'ville': 'Marseille'}

"""
PROTOTYPE AVEC REGISTRY
"""

class PrototypeRegistry:
    """
    Registry de prototypes
    Stocke prototypes préfabriqués
    """
    
    def __init__(self):
        self._prototypes = {}
    
    def enregistrer(self, nom, prototype):
        """Enregistrer un prototype"""
        self._prototypes[nom] = prototype
    
    def obtenir(self, nom):
        """Obtenir clone d'un prototype"""
        prototype = self._prototypes.get(nom)
        if not prototype:
            raise ValueError(f"Prototype '{nom}' non trouvé")
        return prototype.clone()

# Configuration prototypes
class ConfigServeur(Prototype):
    def __init__(self, nom, ip, port):
        self.nom = nom
        self.ip = ip
        self.port = port
    
    def __str__(self):
        return f"{self.nom}: {self.ip}:{self.port}"

# Registry
registry = PrototypeRegistry()

# Enregistrer prototypes
registry.enregistrer("web", ConfigServeur("Web", "192.168.1.10", 80))
registry.enregistrer("db", ConfigServeur("DB", "192.168.1.20", 5432))
registry.enregistrer("cache", ConfigServeur("Cache", "192.168.1.30", 6379))

# Utiliser prototypes
serveur1 = registry.obtenir("web")
serveur1.nom = "Web-Prod"

serveur2 = registry.obtenir("web")
serveur2.nom = "Web-Dev"
serveur2.port = 8080

print(serveur1)  # Web-Prod: 192.168.1.10:80
print(serveur2)  # Web-Dev: 192.168.1.10:8080


# Ce chapitre continue...
# ============================================================================

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 4 (SUITE) : PATTERNS STRUCTURELS
# ============================================================================
#
# [OBJECTIF] CONTENU :
# - Chapitre 16 : Patterns Structurels Complets
#   * Adapter (Compatibilité d'interfaces)
#   * Decorator (Ajout dynamique de fonctionnalités)
#   * Facade (Simplification d'interface)
#   * Proxy (Contrôle d'accès)
#   * Composite (Structures arborescentes)
#
# [TEMPS] TEMPS : ~4-5 heures
# [DOCS] PRÉREQUIS : Chapitre 15 (Patterns Créationnels)
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 16 : PATTERNS STRUCTURELS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Adapter interfaces incompatibles (Adapter)
[OK] Ajouter fonctionnalités dynamiquement (Decorator)
[OK] Simplifier interfaces complexes (Facade)
[OK] Contrôler accès aux objets (Proxy)
[OK] Gérer structures arborescentes (Composite)
[OK] Choisir le bon pattern structurel
"""


# ----------------------------------------------------------------------------
# [PLUGIN] PATTERN ADAPTER
# ----------------------------------------------------------------------------

"""
ADAPTER = Rendre compatibles deux interfaces incompatibles

[IDEE] Analogie :
Adaptateur électrique pour prises différentes
- Appareil européen (2 broches)
- Prise américaine (3 broches)
- Adaptateur fait le lien


CAS D'USAGE :
- Intégrer bibliothèque tierce
- Faire cohabiter ancien et nouveau code
- Utiliser plusieurs APIs similaires


PROBLÈME : INTERFACES INCOMPATIBLES
"""

# Notre interface attendue
class Lecteur:
    def lire(self, fichier):
        pass

# Notre lecteur MP3
class LecteurMP3(Lecteur):
    def lire(self, fichier):
        print(f"[SON] Lecture MP3 : {fichier}")

# Bibliothèque tierce avec interface différente
class LecteurVLC:
    """Bibliothèque externe (interface différente)"""
    def play_video(self, filename):
        print(f"[DEMARRAGE] VLC lecture : {filename}")

# [X] Problème : Incompatibilité
def jouer_media(lecteur: Lecteur, fichier):
    lecteur.lire(fichier)  # LecteurVLC n'a pas de méthode lire() !

# jouer_media(LecteurVLC(), "video.mp4")  # AttributeError

"""
SOLUTION : ADAPTER PATTERN
"""

class AdapterVLC(Lecteur):
    """
    [IDEE] ADAPTER
    
    Adapte l'interface de LecteurVLC à notre interface Lecteur
    """
    
    def __init__(self, lecteur_vlc: LecteurVLC):
        self.lecteur_vlc = lecteur_vlc
    
    def lire(self, fichier):
        """Adapte lire() vers play_video()"""
        self.lecteur_vlc.play_video(fichier)

# [OK] Maintenant compatible !
def jouer_media(lecteur: Lecteur, fichier):
    lecteur.lire(fichier)

# Test
lecteur_mp3 = LecteurMP3()
jouer_media(lecteur_mp3, "song.mp3")
# [SON] Lecture MP3 : song.mp3

lecteur_vlc_adapte = AdapterVLC(LecteurVLC())
jouer_media(lecteur_vlc_adapte, "video.mp4")
# [DEMARRAGE] VLC lecture : video.mp4

"""
EXEMPLE COMPLET : ADAPTER POUR API DE PAIEMENT
"""

# Notre interface standard
class ProcesseurPaiement:
    def traiter_paiement(self, montant):
        pass

# Implémentation maison
class PaymentLocal(ProcesseurPaiement):
    def traiter_paiement(self, montant):
        print(f"[CARTE] Paiement local de {montant}€")
        return True

# API externe Stripe (interface différente)
class StripeAPI:
    """API Stripe (bibliothèque tierce)"""
    def create_charge(self, amount_in_cents, currency="eur"):
        print(f"[CARTE] Stripe charge: {amount_in_cents/100}€ ({currency})")
        return {"status": "succeeded"}

# API externe PayPal (encore différente)
class PayPalAPI:
    """API PayPal (bibliothèque tierce)"""
    def make_payment(self, amount, currency="EUR"):
        print(f"[CARTE] PayPal payment: {amount} {currency}")
        return {"state": "approved"}

# Adapters
class StripeAdapter(ProcesseurPaiement):
    """Adapter pour Stripe"""
    
    def __init__(self, api_key):
        self.stripe = StripeAPI()
        self.api_key = api_key
    
    def traiter_paiement(self, montant):
        # Convertir euros en centimes
        centimes = int(montant * 100)
        result = self.stripe.create_charge(centimes)
        return result["status"] == "succeeded"

class PayPalAdapter(ProcesseurPaiement):
    """Adapter pour PayPal"""
    
    def __init__(self, client_id):
        self.paypal = PayPalAPI()
        self.client_id = client_id
    
    def traiter_paiement(self, montant):
        result = self.paypal.make_payment(montant)
        return result["state"] == "approved"

# Code client (ne connaît que ProcesseurPaiement)
class Boutique:
    def __init__(self, processeur: ProcesseurPaiement):
        self.processeur = processeur
    
    def checkout(self, panier):
        total = sum(item['prix'] for item in panier)
        print(f"\nTotal: {total}€")
        
        if self.processeur.traiter_paiement(total):
            print("[OK] Paiement réussi !\n")
        else:
            print("[X] Paiement échoué\n")

# Test avec différents processeurs
panier = [
    {'nom': 'Livre', 'prix': 15.99},
    {'nom': 'Stylo', 'prix': 2.50}
]

print("=== PAIEMENT LOCAL ===")
boutique = Boutique(PaymentLocal())
boutique.checkout(panier)

print("=== PAIEMENT STRIPE ===")
boutique = Boutique(StripeAdapter("sk_test_123"))
boutique.checkout(panier)

print("=== PAIEMENT PAYPAL ===")
boutique = Boutique(PayPalAdapter("client_abc"))
boutique.checkout(panier)

"""
[OK] AVANTAGES ADAPTER :

1. RÉUTILISATION
   Utiliser code existant sans modification

2. PRINCIPE OUVERT/FERMÉ
   Ajouter adapteurs sans changer code existant

3. DÉCOUPLAGE
   Code client indépendant des implémentations
"""


# ----------------------------------------------------------------------------
# [DESIGN] PATTERN DECORATOR
# ----------------------------------------------------------------------------

"""
DECORATOR = Ajouter fonctionnalités à un objet dynamiquement

[IDEE] Analogie :
Décorer un cadeau
- Cadeau de base
- + Papier cadeau
- + Ruban
- + Carte
Chaque décoration ajoute une couche


CAS D'USAGE :
- Logging
- Validation
- Cache
- Transformation de données
- Middleware


[ATTENTION] NE PAS CONFONDRE AVEC @decorateur Python !
(Concept similaire mais différent)


PROBLÈME : COMBINAISONS EXPLOSIVES DE SOUS-CLASSES
"""

# [X] Approche par héritage (explosion combinatoire)

class Cafe:
    def cout(self):
        return 2.0

class CafeAvecLait(Cafe):
    def cout(self):
        return 2.5

class CafeAvecSucre(Cafe):
    def cout(self):
        return 2.2

class CafeAvecLaitEtSucre(Cafe):  # <- Combinaison
    def cout(self):
        return 2.7

class CafeAvecLaitEtSucreEtCreme(Cafe):  # <- Encore plus de combinaisons !
    def cout(self):
        return 3.2

# [X] Problème : N suppléments = 2^N sous-classes !

"""
SOLUTION : DECORATOR PATTERN
"""

class Boisson:
    """Composant de base"""
    
    def cout(self):
        pass
    
    def description(self):
        pass

class Cafe(Boisson):
    """Café simple"""
    
    def cout(self):
        return 2.0
    
    def description(self):
        return "Café"

class DecorateurBoisson(Boisson):
    """
    [IDEE] DECORATOR DE BASE
    
    Enveloppe une boisson
    """
    
    def __init__(self, boisson: Boisson):
        self.boisson = boisson
    
    def cout(self):
        return self.boisson.cout()
    
    def description(self):
        return self.boisson.description()

class Lait(DecorateurBoisson):
    """Décorateur : Ajoute lait"""
    
    def cout(self):
        return self.boisson.cout() + 0.5
    
    def description(self):
        return self.boisson.description() + " + Lait"

class Sucre(DecorateurBoisson):
    """Décorateur : Ajoute sucre"""
    
    def cout(self):
        return self.boisson.cout() + 0.2
    
    def description(self):
        return self.boisson.description() + " + Sucre"

class Creme(DecorateurBoisson):
    """Décorateur : Ajoute crème"""
    
    def cout(self):
        return self.boisson.cout() + 0.7
    
    def description(self):
        return self.boisson.description() + " + Crème"

# [OK] Utilisation : Combiner facilement !

# Café simple
boisson1 = Cafe()
print(f"{boisson1.description()}: {boisson1.cout()}€")
# Café: 2.0€

# Café avec lait
boisson2 = Lait(Cafe())
print(f"{boisson2.description()}: {boisson2.cout()}€")
# Café + Lait: 2.5€

# Café avec lait et sucre
boisson3 = Sucre(Lait(Cafe()))
print(f"{boisson3.description()}: {boisson3.cout()}€")
# Café + Lait + Sucre: 2.7€

# Café avec lait, sucre et crème
boisson4 = Creme(Sucre(Lait(Cafe())))
print(f"{boisson4.description()}: {boisson4.cout()}€")
# Café + Lait + Sucre + Crème: 3.4€

"""
EXEMPLE COMPLET : DECORATOR POUR LOGGING
"""

class DataSource:
    """Interface pour sources de données"""
    
    def write_data(self, data):
        pass
    
    def read_data(self):
        pass

class FileDataSource(DataSource):
    """Source de données : Fichier"""
    
    def __init__(self, filename):
        self.filename = filename
        self.data = ""
    
    def write_data(self, data):
        self.data = data
        print(f"[DOSSIER] Écriture dans {self.filename}")
    
    def read_data(self):
        print(f"[DOSSIER] Lecture depuis {self.filename}")
        return self.data

class DataSourceDecorator(DataSource):
    """Décorateur de base"""
    
    def __init__(self, source: DataSource):
        self.wrappee = source
    
    def write_data(self, data):
        self.wrappee.write_data(data)
    
    def read_data(self):
        return self.wrappee.read_data()

class LoggingDecorator(DataSourceDecorator):
    """Décorateur : Ajoute logging"""
    
    def write_data(self, data):
        print(f"[NOTE] LOG: Écriture de {len(data)} caractères")
        super().write_data(data)
    
    def read_data(self):
        print(f"[NOTE] LOG: Lecture des données")
        data = super().read_data()
        print(f"[NOTE] LOG: {len(data)} caractères lus")
        return data

class EncryptionDecorator(DataSourceDecorator):
    """Décorateur : Ajoute chiffrement"""
    
    def write_data(self, data):
        encrypted = self._encrypt(data)
        print(f"[SECURISE] Chiffrement ({len(data)} -> {len(encrypted)} chars)")
        super().write_data(encrypted)
    
    def read_data(self):
        encrypted_data = super().read_data()
        decrypted = self._decrypt(encrypted_data)
        print(f"[DEVERROUILLE] Déchiffrement ({len(encrypted_data)} -> {len(decrypted)} chars)")
        return decrypted
    
    def _encrypt(self, data):
        # Simulation simple (ROT13)
        return data[::-1]  # Inverse la chaîne
    
    def _decrypt(self, data):
        return data[::-1]

class CompressionDecorator(DataSourceDecorator):
    """Décorateur : Ajoute compression"""
    
    def write_data(self, data):
        compressed = self._compress(data)
        print(f"[PACKAGE] Compression ({len(data)} -> {len(compressed)} chars)")
        super().write_data(compressed)
    
    def read_data(self):
        compressed_data = super().read_data()
        decompressed = self._decompress(compressed_data)
        print(f"[DOSSIER] Décompression ({len(compressed_data)} -> {len(decompressed)} chars)")
        return decompressed
    
    def _compress(self, data):
        # Simulation simple
        return data.replace(" ", "")
    
    def _decompress(self, data):
        # Simulation simple
        return data

# Test : Combiner plusieurs décorateurs

print("=== SOURCE SIMPLE ===")
source1 = FileDataSource("data.txt")
source1.write_data("Hello World")
print(source1.read_data())

print("\n=== SOURCE AVEC LOGGING ===")
source2 = LoggingDecorator(FileDataSource("data.txt"))
source2.write_data("Hello World")
print(source2.read_data())

print("\n=== SOURCE AVEC LOGGING + CHIFFREMENT ===")
source3 = LoggingDecorator(
    EncryptionDecorator(
        FileDataSource("data.txt")
    )
)
source3.write_data("Hello World")
print(source3.read_data())

print("\n=== SOURCE AVEC LOGGING + CHIFFREMENT + COMPRESSION ===")
source4 = LoggingDecorator(
    EncryptionDecorator(
        CompressionDecorator(
            FileDataSource("data.txt")
        )
    )
)
source4.write_data("Hello World Test")
print(source4.read_data())

"""
[OK] AVANTAGES DECORATOR :

1. FLEXIBILITÉ
   Combiner fonctionnalités à volonté

2. PRINCIPE OUVERT/FERMÉ
   Ajouter comportements sans modifier classes

3. RESPONSABILITÉ UNIQUE
   Chaque décorateur = une fonctionnalité


[ATTENTION] INCONVÉNIENTS :

- Beaucoup de petites classes
- Ordre des décorateurs important
- Complexité si trop de décorateurs
"""


# ----------------------------------------------------------------------------
# [CLASSICAL_BUILDING] PATTERN FACADE
# ----------------------------------------------------------------------------

"""
FACADE = Interface simple pour système complexe

[IDEE] Analogie :
Restaurant
- Client : "Un menu du jour, s'il vous plaît"
- Serveur (facade) gère tout :
  * Commande en cuisine
  * Boisson au bar
  * Facturation en caisse

Client n'a qu'une interface simple


CAS D'USAGE :
- Simplifier bibliothèque complexe
- Unifier plusieurs sous-systèmes
- Point d'entrée unique


PROBLÈME : SYSTÈME COMPLEXE
"""

# Sous-système complexe de Home Cinema

class Amplificateur:
    def allumer(self):
        print("[SON] Amplificateur allumé")
    
    def eteindre(self):
        print("[SON] Amplificateur éteint")
    
    def volume(self, niveau):
        print(f"[SON] Volume: {niveau}")

class Lecteur:
    def allumer(self):
        print("[DISQUE] Lecteur allumé")
    
    def eteindre(self):
        print("[DISQUE] Lecteur éteint")
    
    def lire(self, film):
        print(f"[DISQUE] Lecture: {film}")
    
    def arreter(self):
        print("[DISQUE] Arrêt")

class Projecteur:
    def allumer(self):
        print("[FILM_PROJECTOR] Projecteur allumé")
    
    def eteindre(self):
        print("[FILM_PROJECTOR] Projecteur éteint")
    
    def mode_cinema(self):
        print("[FILM_PROJECTOR] Mode cinéma activé")

class Lumieres:
    def tamiser(self, niveau):
        print(f"[IDEE] Lumières tamisées à {niveau}%")
    
    def allumer(self):
        print("[IDEE] Lumières allumées")

# [X] Client doit connaître tous les sous-systèmes
def regarder_film(film):
    amp = Amplificateur()
    lecteur = Lecteur()
    proj = Projecteur()
    lumieres = Lumieres()
    
    # Procédure complexe !
    lumieres.tamiser(10)
    proj.allumer()
    proj.mode_cinema()
    amp.allumer()
    amp.volume(5)
    lecteur.allumer()
    lecteur.lire(film)
    # ... Beaucoup d'étapes !

"""
SOLUTION : FACADE PATTERN
"""

class HomeCinemaFacade:
    """
    [IDEE] FACADE
    
    Interface simple pour système complexe
    Cache la complexité
    """
    
    def __init__(self):
        self.amp = Amplificateur()
        self.lecteur = Lecteur()
        self.projecteur = Projecteur()
        self.lumieres = Lumieres()
    
    def regarder_film(self, film):
        """
        Une seule méthode simple pour tout faire
        """
        print("[DEMARRAGE] Préparation film...")
        
        self.lumieres.tamiser(10)
        self.projecteur.allumer()
        self.projecteur.mode_cinema()
        self.amp.allumer()
        self.amp.volume(5)
        self.lecteur.allumer()
        self.lecteur.lire(film)
        
        print(f"[DEMARRAGE] Profitez de '{film}' !\n")
    
    def fin_film(self):
        """Tout éteindre simplement"""
        print("[DEMARRAGE] Arrêt du film...")
        
        self.lecteur.arreter()
        self.lecteur.eteindre()
        self.amp.eteindre()
        self.projecteur.eteindre()
        self.lumieres.allumer()
        
        print("[DEMARRAGE] C'est terminé !\n")

# [OK] Client utilise interface simple
cinema = HomeCinemaFacade()

cinema.regarder_film("Inception")
# ... profiter du film ...
cinema.fin_film()

"""
EXEMPLE COMPLET : FACADE POUR SYSTÈME DE COMMANDE
"""

# Sous-systèmes complexes

class GestionStock:
    """Sous-système : Gestion du stock"""
    
    def verifier_disponibilite(self, produit_id, quantite):
        print(f"[PACKAGE] Vérification stock produit {produit_id}: {quantite} unités")
        return True  # Simplifié
    
    def reserver(self, produit_id, quantite):
        print(f"[PACKAGE] Réservation {quantite} unités de {produit_id}")
    
    def liberer(self, produit_id, quantite):
        print(f"[PACKAGE] Libération {quantite} unités de {produit_id}")

class Paiement:
    """Sous-système : Paiement"""
    
    def autoriser(self, montant, carte):
        print(f"[CARTE] Autorisation paiement: {montant}€")
        return True
    
    def capturer(self, montant):
        print(f"[CARTE] Capture paiement: {montant}€")
    
    def rembourser(self, montant):
        print(f"[CARTE] Remboursement: {montant}€")

class Expedition:
    """Sous-système : Expédition"""
    
    def creer_colis(self, produits, adresse):
        print(f"[POSTBOX] Création colis pour {adresse}")
    
    def planifier_livraison(self, colis_id):
        print(f"[POSTBOX] Livraison planifiée: {colis_id}")

class Notification:
    """Sous-système : Notifications"""
    
    def envoyer_confirmation(self, client, numero_commande):
        print(f"[EMAIL] Email confirmation envoyé à {client}: #{numero_commande}")
    
    def envoyer_tracking(self, client, numero_suivi):
        print(f"[EMAIL] Email tracking envoyé à {client}: {numero_suivi}")

# FACADE

class CommandeFacade:
    """
    [IDEE] FACADE pour système de commande
    
    Coordonne tous les sous-systèmes
    Interface simple pour le client
    """
    
    def __init__(self):
        self.stock = GestionStock()
        self.paiement = Paiement()
        self.expedition = Expedition()
        self.notification = Notification()
    
    def passer_commande(self, client, produits, carte, adresse):
        """
        Méthode unique pour passer commande
        Gère toute la complexité en interne
        """
        print(f"\n{'='*60}")
        print(f"NOUVELLE COMMANDE - Client: {client}")
        print(f"{'='*60}\n")
        
        # 1. Vérifier stock
        for produit in produits:
            if not self.stock.verifier_disponibilite(produit['id'], produit['quantite']):
                print("[X] Stock insuffisant")
                return False
        
        # 2. Calculer total
        total = sum(p['prix'] * p['quantite'] for p in produits)
        
        # 3. Traiter paiement
        if not self.paiement.autoriser(total, carte):
            print("[X] Paiement refusé")
            return False
        
        # 4. Réserver stock
        for produit in produits:
            self.stock.reserver(produit['id'], produit['quantite'])
        
        # 5. Capturer paiement
        self.paiement.capturer(total)
        
        # 6. Créer expédition
        self.expedition.creer_colis(produits, adresse)
        numero_suivi = "TRACK123456"
        self.expedition.planifier_livraison(numero_suivi)
        
        # 7. Notifier client
        numero_commande = "CMD001"
        self.notification.envoyer_confirmation(client, numero_commande)
        self.notification.envoyer_tracking(client, numero_suivi)
        
        print(f"\n[OK] Commande {numero_commande} confirmée !\n")
        return True

# Utilisation
facade = CommandeFacade()

commande = [
    {'id': 'PROD1', 'nom': 'Laptop', 'prix': 999, 'quantite': 1},
    {'id': 'PROD2', 'nom': 'Souris', 'prix': 29, 'quantite': 2}
]

facade.passer_commande(
    client="alice@example.com",
    produits=commande,
    carte="1234-****-****-5678",
    adresse="123 Rue Example, Paris"
)

"""
[OK] AVANTAGES FACADE :

1. SIMPLICITÉ
   Interface simple pour système complexe

2. DÉCOUPLAGE
   Client indépendant des sous-systèmes

3. MAINTENANCE
   Modifications internes n'affectent pas client


[ATTENTION] FAÇADE ≠ DISSIMULATION

Facade simplifie mais n'empêche pas accès direct aux sous-systèmes
"""


# ----------------------------------------------------------------------------
# [SECURITE] PATTERN PROXY
# ----------------------------------------------------------------------------

"""
PROXY = Substitut/représentant d'un autre objet

[IDEE] Analogie :
Chèque = Proxy pour argent réel
- Représente l'argent
- Contrôle accès à l'argent
- Ajoute vérifications


TYPES DE PROXY :

1. REMOTE PROXY
   Représente objet distant (API, réseau)

2. VIRTUAL PROXY  
   Création différée (lazy loading)

3. PROTECTION PROXY
   Contrôle accès (permissions)

4. CACHE PROXY
   Met en cache résultats


CAS D'USAGE :
- Lazy loading
- Contrôle d'accès
- Logging
- Cache


PROBLÈME : OBJET COÛTEUX À CRÉER
"""

import time

class ImageHauteResolution:
    """Image lourde à charger"""
    
    def __init__(self, filename):
        self.filename = filename
        self._charger()
    
    def _charger(self):
        print(f"[ENTREE] Chargement de {self.filename}... (opération lente)")
        time.sleep(2)  # Simulation
        print(f"[OK] {self.filename} chargé")
    
    def afficher(self):
        print(f"[FRAME_WITH_PICTURE] Affichage de {self.filename}")

# [X] Problème : Images chargées même si pas affichées
images = [
    ImageHauteResolution("photo1.jpg"),
    ImageHauteResolution("photo2.jpg"),
    ImageHauteResolution("photo3.jpg")
]
# Attente de 6 secondes alors qu'on n'affiche rien encore !

"""
SOLUTION : VIRTUAL PROXY (Lazy Loading)
"""

class ImageProxy:
    """
    [IDEE] VIRTUAL PROXY
    
    Retarde chargement jusqu'à utilisation réelle
    """
    
    def __init__(self, filename):
        self.filename = filename
        self._image_reelle = None
    
    def afficher(self):
        # Charger seulement si nécessaire
        if self._image_reelle is None:
            self._image_reelle = ImageHauteResolution(self.filename)
        
        self._image_reelle.afficher()

# [OK] Images créées instantanément
print("Création des proxies (instantané)...")
images = [
    ImageProxy("photo1.jpg"),
    ImageProxy("photo2.jpg"),
    ImageProxy("photo3.jpg")
]
print("[OK] Proxies créés\n")

# Charger seulement quand nécessaire
print("Affichage de la première image:")
images[0].afficher()  # Charge et affiche
print()

print("Réaffichage de la première image:")
images[0].afficher()  # Déjà chargé, affiche directement
print()

print("Affichage de la deuxième image:")
images[1].afficher()  # Charge et affiche

"""
PROTECTION PROXY : CONTRÔLE D'ACCÈS
"""

class Document:
    """Document sensible"""
    
    def __init__(self, contenu):
        self.contenu = contenu
    
    def lire(self):
        return self.contenu
    
    def ecrire(self, nouveau_contenu):
        self.contenu = nouveau_contenu

class DocumentProxy:
    """
    [IDEE] PROTECTION PROXY
    
    Contrôle accès selon permissions
    """
    
    def __init__(self, document, utilisateur, permissions):
        self._document = document
        self._utilisateur = utilisateur
        self._permissions = permissions
    
    def lire(self):
        if 'lecture' in self._permissions:
            print(f"[OK] {self._utilisateur} lit le document")
            return self._document.lire()
        else:
            raise PermissionError(f"[X] {self._utilisateur} n'a pas la permission de lecture")
    
    def ecrire(self, nouveau_contenu):
        if 'ecriture' in self._permissions:
            print(f"[OK] {self._utilisateur} modifie le document")
            self._document.ecrire(nouveau_contenu)
        else:
            raise PermissionError(f"[X] {self._utilisateur} n'a pas la permission d'écriture")

# Test
doc = Document("Contenu confidentiel")

# Utilisateur avec lecture seule
proxy_lecture = DocumentProxy(doc, "Bob", permissions=['lecture'])
print(proxy_lecture.lire())  # [OK] OK

try:
    proxy_lecture.ecrire("Nouveau contenu")  # [X] Erreur
except PermissionError as e:
    print(e)

# Utilisateur avec lecture et écriture
proxy_complet = DocumentProxy(doc, "Admin", permissions=['lecture', 'ecriture'])
print(proxy_complet.lire())  # [OK] OK
proxy_complet.ecrire("Contenu modifié")  # [OK] OK

"""
CACHE PROXY : MISE EN CACHE
"""

class ServiceAPI:
    """Service API (requêtes lentes)"""
    
    def obtenir_donnees(self, id):
        print(f"[WEB] Requête API pour ID {id}... (lent)")
        time.sleep(1)  # Simulation réseau
        return f"Données pour {id}"

class CacheProxy:
    """
    [IDEE] CACHE PROXY
    
    Met en cache résultats d'API
    """
    
    def __init__(self, service):
        self._service = service
        self._cache = {}
    
    def obtenir_donnees(self, id):
        if id in self._cache:
            print(f"[SAUVEGARDE] Cache HIT pour ID {id}")
            return self._cache[id]
        
        print(f"[SAUVEGARDE] Cache MISS pour ID {id}")
        resultat = self._service.obtenir_donnees(id)
        self._cache[id] = resultat
        return resultat

# Test
api_avec_cache = CacheProxy(ServiceAPI())

# Première requête : appel API
print(api_avec_cache.obtenir_donnees(1))

# Deuxième requête : depuis cache
print(api_avec_cache.obtenir_donnees(1))

# Nouvelle donnée : appel API
print(api_avec_cache.obtenir_donnees(2))


# ----------------------------------------------------------------------------
# [ARBRE] PATTERN COMPOSITE
# ----------------------------------------------------------------------------

"""
COMPOSITE = Traiter objets individuels et compositions d'objets uniformément

[IDEE] Analogie :
Structure organisationnelle
- Employé (feuille)
- Manager (composite : a des employés)
- CEO (composite de managers)
Traiter tous comme "personne"


CAS D'USAGE :
- Structures arborescentes
- Système de fichiers
- Interface graphique (widgets)
- Menus


PROBLÈME : TRAITER DIFFÉREMMENT INDIVIDUS ET GROUPES
"""

# [X] Sans Composite

class Fichier:
    def __init__(self, nom):
        self.nom = nom
    
    def afficher(self):
        print(f"[FICHIER] {self.nom}")

class Dossier:
    def __init__(self, nom):
        self.nom = nom
        self.contenu = []
    
    def ajouter(self, element):
        self.contenu.append(element)
    
    def afficher(self):
        print(f"[DOSSIER] {self.nom}")
        for element in self.contenu:
            # [X] Doit différencier Fichier et Dossier
            if isinstance(element, Fichier):
                element.afficher()
            elif isinstance(element, Dossier):
                element.afficher()

"""
SOLUTION : COMPOSITE PATTERN
"""

from abc import ABC, abstractmethod

class ElementSysteme(ABC):
    """
    [IDEE] COMPONENT (Interface commune)
    
    Fichiers ET dossiers implémentent cette interface
    """
    
    @abstractmethod
    def afficher(self, indent=0):
        pass

class Fichier(ElementSysteme):
    """LEAF (Feuille) : Élément terminal"""
    
    def __init__(self, nom):
        self.nom = nom
    
    def afficher(self, indent=0):
        print("  " * indent + f"[FICHIER] {self.nom}")

class Dossier(ElementSysteme):
    """COMPOSITE : Contient d'autres éléments"""
    
    def __init__(self, nom):
        self.nom = nom
        self.elements = []
    
    def ajouter(self, element: ElementSysteme):
        """Ajouter fichier ou sous-dossier"""
        self.elements.append(element)
    
    def retirer(self, element: ElementSysteme):
        """Retirer élément"""
        self.elements.remove(element)
    
    def afficher(self, indent=0):
        """Afficher récursivement"""
        print("  " * indent + f"[DOSSIER] {self.nom}")
        for element in self.elements:
            # [OK] Traite tous les éléments pareil !
            element.afficher(indent + 1)

# Construction arborescence
racine = Dossier("Projets")

dossier_python = Dossier("Python")
dossier_python.ajouter(Fichier("main.py"))
dossier_python.ajouter(Fichier("utils.py"))

dossier_java = Dossier("Java")
dossier_java.ajouter(Fichier("Main.java"))

sous_dossier = Dossier("lib")
sous_dossier.ajouter(Fichier("library.java"))
dossier_java.ajouter(sous_dossier)

racine.ajouter(dossier_python)
racine.ajouter(dossier_java)
racine.ajouter(Fichier("README.md"))

# Affichage
racine.afficher()

# Sortie :
# [DOSSIER] Projets
#   [DOSSIER] Python
#     [FICHIER] main.py
#     [FICHIER] utils.py
#   [DOSSIER] Java
#     [FICHIER] Main.java
#     [DOSSIER] lib
#       [FICHIER] library.java
#   [FICHIER] README.md

"""
EXEMPLE COMPLET : MENU HIÉRARCHIQUE
"""

class MenuComponent(ABC):
    """Component de base"""
    
    @abstractmethod
    def afficher(self):
        pass
    
    @abstractmethod
    def obtenir_prix(self):
        pass

class MenuItem(MenuComponent):
    """Leaf : Plat individuel"""
    
    def __init__(self, nom, prix):
        self.nom = nom
        self.prix = prix
    
    def afficher(self):
        print(f"  [FORK_AND_KNIFE_WITH_PLATE] {self.nom} - {self.prix}€")
    
    def obtenir_prix(self):
        return self.prix

class Menu(MenuComponent):
    """Composite : Menu ou sous-menu"""
    
    def __init__(self, nom):
        self.nom = nom
        self.items = []
    
    def ajouter(self, item: MenuComponent):
        self.items.append(item)
    
    def afficher(self):
        print(f"\n[LISTE] {self.nom}")
        print("=" * 40)
        for item in self.items:
            item.afficher()
        print(f"Total: {self.obtenir_prix()}€")
    
    def obtenir_prix(self):
        """Prix total récursif"""
        return sum(item.obtenir_prix() for item in self.items)

# Construction menu restaurant
menu_principal = Menu("Menu Principal")

# Entrées
menu_entrees = Menu("Entrées")
menu_entrees.ajouter(MenuItem("Salade César", 8.5))
menu_entrees.ajouter(MenuItem("Soupe à l'oignon", 6.0))

# Plats
menu_plats = Menu("Plats")
menu_plats.ajouter(MenuItem("Steak frites", 18.0))
menu_plats.ajouter(MenuItem("Saumon grillé", 22.0))

# Desserts
menu_desserts = Menu("Desserts")
menu_desserts.ajouter(MenuItem("Tarte tatin", 7.0))
menu_desserts.ajouter(MenuItem("Crème brûlée", 6.5))

menu_principal.ajouter(menu_entrees)
menu_principal.ajouter(menu_plats)
menu_principal.ajouter(menu_desserts)

# Affichage
menu_principal.afficher()

"""
[OK] AVANTAGES COMPOSITE :

1. UNIFORMITÉ
   Traiter feuilles et composites pareil

2. EXTENSIBILITÉ
   Facile d'ajouter nouveaux composants

3. SIMPLICITÉ CLIENT
   Code client simple (récursion gérée)


Ce chapitre continue...
"""

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 4 (SUITE) : PATTERNS COMPORTEMENTAUX
# ============================================================================
#
# [OBJECTIF] CONTENU :
# - Chapitre 17 : Patterns Comportementaux Complets
#   * Observer (Pub/Sub, Événements)
#   * Strategy (Algorithmes interchangeables)
#   * Command (Encapsulation actions, Undo/Redo)
#   * State (Gestion d'états)
#   * Template Method (Squelette d'algorithme)
#   * Iterator (Parcours de collections)
#   * Chain of Responsibility (Chaîne de traitement)
#
# [TEMPS] TEMPS : ~5-6 heures
# [DOCS] PRÉREQUIS : Chapitres 15-16 (Patterns Créationnels et Structurels)
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 17 : PATTERNS COMPORTEMENTAUX
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Implémenter système d'événements (Observer)
[OK] Rendre algorithmes interchangeables (Strategy)
[OK] Encapsuler actions avec undo/redo (Command)
[OK] Gérer changements d'état (State)
[OK] Définir squelette d'algorithme (Template Method)
[OK] Créer itérateurs personnalisés (Iterator)
[OK] Construire chaîne de traitement (Chain of Responsibility)
"""


# ----------------------------------------------------------------------------
# [EYE] PATTERN OBSERVER
# ----------------------------------------------------------------------------

"""
OBSERVER = Notifier automatiquement plusieurs objets d'un changement d'état

[IDEE] Analogie :
Chaîne YouTube
- Créateur (Subject) publie vidéo
- Abonnés (Observers) notifiés automatiquement
- Ajout/retrait abonnés dynamique


AUSSI APPELÉ : Pub/Sub (Publisher/Subscriber)


CAS D'USAGE :
- Systèmes d'événements
- Interface graphique (MVC)
- Notifications temps réel
- Logging distribué


PROBLÈME : COUPLAGE FORT
"""

# [X] Sans Observer (couplage direct)

class Produit:
    def __init__(self, nom, prix):
        self.nom = nom
        self.prix = prix
    
    def changer_prix(self, nouveau_prix):
        self.prix = nouveau_prix
        
        # [X] Couplage : Produit connaît affichage et logger
        print(f"Affichage: Prix de {self.nom} changé à {self.prix}€")
        print(f"LOG: Changement prix {self.nom}")
        # Et si on veut ajouter email, SMS, etc. ?

"""
SOLUTION : OBSERVER PATTERN
"""

from abc import ABC, abstractmethod
from typing import List

class Observer(ABC):
    """
    [IDEE] OBSERVER (Interface)
    
    Définit comment être notifié
    """
    
    @abstractmethod
    def update(self, subject):
        """Appelé quand le subject change"""
        pass

class Subject:
    """
    [IDEE] SUBJECT (Observable)
    
    Maintient liste d'observers
    Notifie lors de changements
    """
    
    def __init__(self):
        self._observers: List[Observer] = []
    
    def attach(self, observer: Observer):
        """Abonner un observer"""
        if observer not in self._observers:
            self._observers.append(observer)
            print(f"[OK] Observer ajouté: {observer.__class__.__name__}")
    
    def detach(self, observer: Observer):
        """Désabonner un observer"""
        self._observers.remove(observer)
        print(f"[X] Observer retiré: {observer.__class__.__name__}")
    
    def notify(self):
        """Notifier tous les observers"""
        print(f"\n[ANNONCE] Notification de {len(self._observers)} observer(s)...")
        for observer in self._observers:
            observer.update(self)

# Subject concret
class Produit(Subject):
    """Produit observable"""
    
    def __init__(self, nom, prix):
        super().__init__()
        self.nom = nom
        self._prix = prix
    
    @property
    def prix(self):
        return self._prix
    
    @prix.setter
    def prix(self, nouveau_prix):
        """Notifie automatiquement au changement"""
        if nouveau_prix != self._prix:
            self._prix = nouveau_prix
            self.notify()  # <- Notifier observers

# Observers concrets
class AffichageObserver(Observer):
    """Observer : Affichage console"""
    
    def update(self, subject: Produit):
        print(f"  [TELEVISION] Affichage: {subject.nom} maintenant à {subject.prix}€")

class LoggerObserver(Observer):
    """Observer : Logging"""
    
    def update(self, subject: Produit):
        print(f"  [NOTE] LOG: Changement prix {subject.nom} -> {subject.prix}€")

class EmailObserver(Observer):
    """Observer : Email"""
    
    def __init__(self, email):
        self.email = email
    
    def update(self, subject: Produit):
        print(f"  [EMAIL] Email à {self.email}: {subject.nom} en promo à {subject.prix}€!")

class StatistiquesObserver(Observer):
    """Observer : Statistiques"""
    
    def __init__(self):
        self.changements = 0
    
    def update(self, subject: Produit):
        self.changements += 1
        print(f"  [GRAPHIQUE] Stats: {self.changements} changement(s) de prix enregistré(s)")

# Utilisation
print("=== CRÉATION PRODUIT ET OBSERVERS ===")
produit = Produit("Laptop", 999)

affichage = AffichageObserver()
logger = LoggerObserver()
email = EmailObserver("client@example.com")
stats = StatistiquesObserver()

# Abonnements
produit.attach(affichage)
produit.attach(logger)
produit.attach(email)
produit.attach(stats)

print("\n=== CHANGEMENT PRIX 1 ===")
produit.prix = 899  # Tous les observers notifiés !

print("\n=== CHANGEMENT PRIX 2 ===")
produit.prix = 799

print("\n=== DÉSABONNEMENT EMAIL ===")
produit.detach(email)

print("\n=== CHANGEMENT PRIX 3 ===")
produit.prix = 699  # Email ne reçoit plus de notifications

"""
[OK] AVANTAGES OBSERVER :

1. DÉCOUPLAGE
   Subject ne connaît pas observers concrets

2. DYNAMIQUE
   Ajouter/retirer observers à runtime

3. BROADCAST
   Un changement -> plusieurs réactions


EXEMPLE COMPLET : SYSTÈME DE TRADING
"""

class Action(Subject):
    """Action en bourse"""
    
    def __init__(self, symbole, prix):
        super().__init__()
        self.symbole = symbole
        self._prix = prix
        self._historique = [prix]
    
    @property
    def prix(self):
        return self._prix
    
    @prix.setter
    def prix(self, nouveau_prix):
        self._prix = nouveau_prix
        self._historique.append(nouveau_prix)
        self.notify()
    
    def variation(self):
        """Variation depuis prix initial"""
        if len(self._historique) < 2:
            return 0
        return ((self._prix - self._historique[0]) / self._historique[0]) * 100

class InvestisseurObserver(Observer):
    """Investisseur surveillant action"""
    
    def __init__(self, nom, seuil_achat, seuil_vente):
        self.nom = nom
        self.seuil_achat = seuil_achat
        self.seuil_vente = seuil_vente
        self.possede = False
    
    def update(self, action: Action):
        if action.prix <= self.seuil_achat and not self.possede:
            print(f"  [ARGENT] {self.nom}: ACHAT {action.symbole} à {action.prix}€")
            self.possede = True
        elif action.prix >= self.seuil_vente and self.possede:
            print(f"  [ARGENT] {self.nom}: VENTE {action.symbole} à {action.prix}€")
            self.possede = False

class AlertePrixObserver(Observer):
    """Alerte si variation importante"""
    
    def update(self, action: Action):
        variation = action.variation()
        if abs(variation) >= 5:
            print(f"  [ALERTE] ALERTE: {action.symbole} variation de {variation:.1f}%")

# Test
print("\n=== TRADING SYSTEM ===")
apple = Action("AAPL", 150)

investisseur1 = InvestisseurObserver("Alice", seuil_achat=145, seuil_vente=160)
investisseur2 = InvestisseurObserver("Bob", seuil_achat=140, seuil_vente=165)
alerte = AlertePrixObserver()

apple.attach(investisseur1)
apple.attach(investisseur2)
apple.attach(alerte)

print("\n--- Prix monte ---")
apple.prix = 155
apple.prix = 160
apple.prix = 165

print("\n--- Prix baisse ---")
apple.prix = 145
apple.prix = 140


# ----------------------------------------------------------------------------
# [OBJECTIF] PATTERN STRATEGY
# ----------------------------------------------------------------------------

"""
STRATEGY = Définir famille d'algorithmes interchangeables

[IDEE] Analogie :
Moyens de transport pour aller au travail
- Voiture (rapide, cher)
- Vélo (lent, gratuit)
- Transport public (moyen, moyen)
Choisir stratégie selon situation


CAS D'USAGE :
- Algorithmes de tri/recherche
- Calculs de prix (réductions)
- Compression de données
- Validation


PROBLÈME : MULTIPLES IF/ELSE
"""

# [X] Sans Strategy

class CalculateurPrix:
    def calculer(self, prix, type_client):
        if type_client == "normal":
            return prix
        elif type_client == "membre":
            return prix * 0.95  # -5%
        elif type_client == "vip":
            return prix * 0.90  # -10%
        elif type_client == "premium":
            return prix * 0.85  # -15%
        else:
            return prix
        # [X] Ajouter type -> modifier cette méthode

"""
SOLUTION : STRATEGY PATTERN
"""

from abc import ABC, abstractmethod

class StrategiePrix(ABC):
    """
    [IDEE] STRATEGY (Interface)
    
    Définit algorithme de calcul prix
    """
    
    @abstractmethod
    def calculer_prix(self, prix_base: float) -> float:
        pass

# Stratégies concrètes
class PrixNormal(StrategiePrix):
    """Pas de réduction"""
    
    def calculer_prix(self, prix_base):
        return prix_base

class PrixMembre(StrategiePrix):
    """Réduction membre : 5%"""
    
    def calculer_prix(self, prix_base):
        return prix_base * 0.95

class PrixVIP(StrategiePrix):
    """Réduction VIP : 10%"""
    
    def calculer_prix(self, prix_base):
        return prix_base * 0.90

class PrixPremium(StrategiePrix):
    """Réduction Premium : 15%"""
    
    def calculer_prix(self, prix_base):
        return prix_base * 0.85

class PrixPromotion(StrategiePrix):
    """Promotion avec pourcentage personnalisé"""
    
    def __init__(self, pourcentage):
        self.pourcentage = pourcentage
    
    def calculer_prix(self, prix_base):
        return prix_base * (1 - self.pourcentage / 100)

# Contexte utilisant strategy
class Commande:
    """
    [IDEE] CONTEXT
    
    Utilise une stratégie de prix
    """
    
    def __init__(self, strategie: StrategiePrix):
        self._strategie = strategie
        self.items = []
    
    def set_strategie(self, strategie: StrategiePrix):
        """Changer stratégie à runtime"""
        self._strategie = strategie
    
    def ajouter_item(self, nom, prix):
        self.items.append({'nom': nom, 'prix': prix})
    
    def calculer_total(self):
        """Calcul avec stratégie actuelle"""
        sous_total = sum(item['prix'] for item in self.items)
        return self._strategie.calculer_prix(sous_total)

# Utilisation
items = [
    ('Laptop', 999),
    ('Souris', 29),
    ('Clavier', 89)
]

print("=== STRATÉGIES DE PRIX ===\n")

# Client normal
commande = Commande(PrixNormal())
for nom, prix in items:
    commande.ajouter_item(nom, prix)
print(f"Client Normal: {commande.calculer_total()}€")

# Client membre
commande.set_strategie(PrixMembre())
print(f"Client Membre (-5%): {commande.calculer_total()}€")

# Client VIP
commande.set_strategie(PrixVIP())
print(f"Client VIP (-10%): {commande.calculer_total()}€")

# Client Premium
commande.set_strategie(PrixPremium())
print(f"Client Premium (-15%): {commande.calculer_total()}€")

# Promotion spéciale
commande.set_strategie(PrixPromotion(20))
print(f"Promotion -20%: {commande.calculer_total()}€")

"""
EXEMPLE COMPLET : STRATÉGIES DE TRI
"""

class StrategieTri(ABC):
    """Interface pour algorithmes de tri"""
    
    @abstractmethod
    def trier(self, data: list) -> list:
        pass

class TriRapide(StrategieTri):
    """Tri rapide (Quick Sort)"""
    
    def trier(self, data):
        print("  [RAPIDE] Utilisation du tri rapide")
        return sorted(data)  # Simplifié

class TriFusion(StrategieTri):
    """Tri fusion (Merge Sort)"""
    
    def trier(self, data):
        print("  [SYNC] Utilisation du tri fusion")
        return sorted(data)  # Simplifié

class TriBulles(StrategieTri):
    """Tri à bulles (Bubble Sort)"""
    
    def trier(self, data):
        print("  [BUBBLES] Utilisation du tri à bulles")
        return sorted(data)  # Simplifié

class GestionnaireDonnees:
    """Contexte avec stratégie de tri configurable"""
    
    def __init__(self, strategie: StrategieTri):
        self.strategie = strategie
    
    def trier_donnees(self, data):
        """Trier avec stratégie actuelle"""
        return self.strategie.trier(data.copy())

# Choix dynamique de stratégie selon taille
def choisir_strategie_tri(taille):
    """Factory pour choisir meilleur algorithme"""
    if taille < 10:
        return TriBulles()  # Simple pour petites listes
    elif taille < 1000:
        return TriRapide()  # Rapide pour taille moyenne
    else:
        return TriFusion()  # Stable pour grandes listes

# Test
donnees = [64, 34, 25, 12, 22, 11, 90]

print("\n=== STRATÉGIES DE TRI ===")
print(f"Données: {donnees}\n")

gestionnaire = GestionnaireDonnees(choisir_strategie_tri(len(donnees)))
resultat = gestionnaire.trier_donnees(donnees)
print(f"Résultat: {resultat}")


# ----------------------------------------------------------------------------
# [RAPIDE] PATTERN COMMAND
# ----------------------------------------------------------------------------

"""
COMMAND = Encapsuler requête comme objet

[IDEE] Analogie :
Restaurant
- Commande écrite (Command) : encapsule demande
- Serveur (Invoker) : exécute commande
- Cuisinier (Receiver) : fait le travail
- Annuler commande possible


CAS D'USAGE :
- Undo/Redo
- Macros et scripts
- File d'attente de tâches
- Transactions


PROBLÈME : ACTIONS NON ANNULABLES
"""

# [X] Sans Command

class Editeur:
    def __init__(self):
        self.texte = ""
    
    def ecrire(self, texte):
        self.texte += texte
        # [X] Comment annuler ?

"""
SOLUTION : COMMAND PATTERN
"""

from abc import ABC, abstractmethod

class Command(ABC):
    """
    [IDEE] COMMAND (Interface)
    
    Encapsule action et son annulation
    """
    
    @abstractmethod
    def execute(self):
        """Exécuter la commande"""
        pass
    
    @abstractmethod
    def undo(self):
        """Annuler la commande"""
        pass

class Editeur:
    """
    [IDEE] RECEIVER
    
    Sait comment faire le travail
    """
    
    def __init__(self):
        self.texte = ""
    
    def ajouter_texte(self, texte):
        self.texte += texte
        print(f"  [EDIT] Texte: '{self.texte}'")
    
    def supprimer_texte(self, longueur):
        self.texte = self.texte[:-longueur]
        print(f"  [EDIT] Texte: '{self.texte}'")

# Commandes concrètes
class CommandeEcrire(Command):
    """Commande : Écrire du texte"""
    
    def __init__(self, editeur: Editeur, texte: str):
        self.editeur = editeur
        self.texte = texte
    
    def execute(self):
        print(f"[NOTE] Écriture de '{self.texte}'")
        self.editeur.ajouter_texte(self.texte)
    
    def undo(self):
        print(f"<- Annulation écriture de '{self.texte}'")
        self.editeur.supprimer_texte(len(self.texte))

class CommandeEffacer(Command):
    """Commande : Effacer texte"""
    
    def __init__(self, editeur: Editeur, longueur: int):
        self.editeur = editeur
        self.longueur = longueur
        self.texte_supprime = ""
    
    def execute(self):
        # Sauvegarder texte supprimé pour undo
        self.texte_supprime = self.editeur.texte[-self.longueur:]
        print(f"[SUPPRIMER] Effacement de '{self.texte_supprime}'")
        self.editeur.supprimer_texte(self.longueur)
    
    def undo(self):
        print(f"<- Annulation effacement, restauration de '{self.texte_supprime}'")
        self.editeur.ajouter_texte(self.texte_supprime)

class HistoriqueCommandes:
    """
    [IDEE] INVOKER
    
    Gère historique et undo/redo
    """
    
    def __init__(self):
        self.historique = []
        self.position = -1
    
    def executer(self, command: Command):
        """Exécuter et ajouter à l'historique"""
        command.execute()
        
        # Supprimer commandes après position actuelle (si on a fait undo)
        self.historique = self.historique[:self.position + 1]
        
        self.historique.append(command)
        self.position += 1
    
    def undo(self):
        """Annuler dernière commande"""
        if self.position >= 0:
            command = self.historique[self.position]
            command.undo()
            self.position -= 1
        else:
            print("[ATTENTION] Rien à annuler")
    
    def redo(self):
        """Refaire commande annulée"""
        if self.position < len(self.historique) - 1:
            self.position += 1
            command = self.historique[self.position]
            command.execute()
        else:
            print("[ATTENTION] Rien à refaire")

# Utilisation
print("\n=== ÉDITEUR AVEC UNDO/REDO ===\n")

editeur = Editeur()
historique = HistoriqueCommandes()

# Exécuter commandes
historique.executer(CommandeEcrire(editeur, "Bonjour "))
historique.executer(CommandeEcrire(editeur, "le "))
historique.executer(CommandeEcrire(editeur, "monde!"))

print("\n--- Undo x2 ---")
historique.undo()
historique.undo()

print("\n--- Redo x1 ---")
historique.redo()

print("\n--- Nouvelle commande ---")
historique.executer(CommandeEcrire(editeur, "Python!"))

"""
EXEMPLE COMPLET : COMMANDES DE JEU
"""

class Personnage:
    """Receiver : Personnage de jeu"""
    
    def __init__(self, nom):
        self.nom = nom
        self.x = 0
        self.y = 0
        self.vie = 100
    
    def deplacer(self, dx, dy):
        self.x += dx
        self.y += dy
        print(f"  [RUNNER] {self.nom} se déplace à ({self.x}, {self.y})")
    
    def subir_degats(self, degats):
        self.vie -= degats
        print(f"  [BROKEN_HEART] {self.nom} perd {degats} PV (vie: {self.vie})")
    
    def soigner(self, points):
        self.vie = min(100, self.vie + points)
        print(f"  [GREEN_HEART] {self.nom} récupère {points} PV (vie: {self.vie})")

class CommandeDeplacement(Command):
    """Commande : Déplacement"""
    
    def __init__(self, personnage: Personnage, dx: int, dy: int):
        self.personnage = personnage
        self.dx = dx
        self.dy = dy
    
    def execute(self):
        print(f"-> Déplacement de ({self.dx}, {self.dy})")
        self.personnage.deplacer(self.dx, self.dy)
    
    def undo(self):
        print(f"<- Annulation déplacement")
        self.personnage.deplacer(-self.dx, -self.dy)

class CommandeAttaque(Command):
    """Commande : Attaque"""
    
    def __init__(self, attaquant: Personnage, cible: Personnage, degats: int):
        self.attaquant = attaquant
        self.cible = cible
        self.degats = degats
    
    def execute(self):
        print(f"[COMBAT] {self.attaquant.nom} attaque {self.cible.nom}")
        self.cible.subir_degats(self.degats)
    
    def undo(self):
        print(f"<- Annulation attaque")
        self.cible.soigner(self.degats)

# Test
print("\n=== COMMANDES DE JEU ===\n")

hero = Personnage("Héros")
monstre = Personnage("Monstre")
historique_jeu = HistoriqueCommandes()

historique_jeu.executer(CommandeDeplacement(hero, 5, 3))
historique_jeu.executer(CommandeDeplacement(hero, 2, 1))
historique_jeu.executer(CommandeAttaque(hero, monstre, 20))

print("\n--- Undo attaque ---")
historique_jeu.undo()

print("\n--- Undo déplacement ---")
historique_jeu.undo()


# ----------------------------------------------------------------------------
# [SYNC] PATTERN STATE
# ----------------------------------------------------------------------------

"""
STATE = Changer comportement d'un objet selon son état interne

[IDEE] Analogie :
Téléphone
- État Verrouillé : appuyer boutons -> rien
- État Déverrouillé : appuyer boutons -> actions
Même action, comportement différent selon état


CAS D'USAGE :
- Machines à états
- Protocoles réseau
- Workflow
- Jeux vidéo


PROBLÈME : IF/ELSE COMPLEXES PARTOUT
"""

# [X] Sans State

class Document:
    def __init__(self):
        self.etat = "brouillon"  # brouillon, moderation, publie
    
    def publier(self):
        if self.etat == "brouillon":
            self.etat = "moderation"
            print("Document envoyé en modération")
        elif self.etat == "moderation":
            self.etat = "publie"
            print("Document publié")
        elif self.etat == "publie":
            print("Déjà publié")
    
    def rejeter(self):
        if self.etat == "brouillon":
            print("Impossible de rejeter un brouillon")
        elif self.etat == "moderation":
            self.etat = "brouillon"
            print("Document rejeté")
        elif self.etat == "publie":
            print("Impossible de rejeter un document publié")
    # [X] Beaucoup de if/else, difficile à maintenir

"""
SOLUTION : STATE PATTERN
"""

from abc import ABC, abstractmethod

class EtatDocument(ABC):
    """
    [IDEE] STATE (Interface)
    
    Définit comportement pour chaque état
    """
    
    @abstractmethod
    def publier(self, document):
        pass
    
    @abstractmethod
    def rejeter(self, document):
        pass

class Document:
    """
    [IDEE] CONTEXT
    
    Délègue comportement à l'état actuel
    """
    
    def __init__(self):
        self._etat = EtatBrouillon()  # État initial
        self.contenu = ""
    
    def set_etat(self, etat: EtatDocument):
        """Changer d'état"""
        print(f"  [SYNC] Changement état: {etat.__class__.__name__}")
        self._etat = etat
    
    def publier(self):
        """Déléguer à l'état actuel"""
        self._etat.publier(self)
    
    def rejeter(self):
        """Déléguer à l'état actuel"""
        self._etat.rejeter(self)

# États concrets
class EtatBrouillon(EtatDocument):
    """État : Brouillon"""
    
    def publier(self, document: Document):
        print("[NOTE] Brouillon -> Modération")
        document.set_etat(EtatModeration())
    
    def rejeter(self, document: Document):
        print("[ATTENTION] Impossible de rejeter un brouillon")

class EtatModeration(EtatDocument):
    """État : En modération"""
    
    def publier(self, document: Document):
        print("[OK] Modération -> Publié")
        document.set_etat(EtatPublie())
    
    def rejeter(self, document: Document):
        print("[X] Modération -> Brouillon (rejeté)")
        document.set_etat(EtatBrouillon())

class EtatPublie(EtatDocument):
    """État : Publié"""
    
    def publier(self, document: Document):
        print("ℹ Document déjà publié")
    
    def rejeter(self, document: Document):
        print("[ATTENTION] Impossible de rejeter un document publié")

# Utilisation
print("\n=== WORKFLOW DOCUMENT ===\n")

doc = Document()

print("Action: Publier brouillon")
doc.publier()  # Brouillon -> Modération

print("\nAction: Publier document en modération")
doc.publier()  # Modération -> Publié

print("\nAction: Publier document déjà publié")
doc.publier()  # Déjà publié

print("\n--- Nouveau document ---")
doc2 = Document()

print("Action: Publier brouillon")
doc2.publier()  # Brouillon -> Modération

print("\nAction: Rejeter document en modération")
doc2.rejeter()  # Modération -> Brouillon

"""
EXEMPLE COMPLET : LECTEUR AUDIO
"""

class EtatLecteur(ABC):
    """État du lecteur"""
    
    @abstractmethod
    def play(self, lecteur):
        pass
    
    @abstractmethod
    def pause(self, lecteur):
        pass
    
    @abstractmethod
    def stop(self, lecteur):
        pass

class LecteurAudio:
    """Lecteur audio avec états"""
    
    def __init__(self):
        self._etat = EtatArrete()
        self.piste = None
    
    def set_etat(self, etat: EtatLecteur):
        self._etat = etat
    
    def play(self):
        self._etat.play(self)
    
    def pause(self):
        self._etat.pause(self)
    
    def stop(self):
        self._etat.stop(self)
    
    def charger_piste(self, piste):
        self.piste = piste
        print(f"[DISQUE] Piste chargée: {piste}")

class EtatArrete(EtatLecteur):
    """État : Arrêté"""
    
    def play(self, lecteur: LecteurAudio):
        print("[BLACK_RIGHT-POINTING_TRIANGLE] Lecture démarrée")
        lecteur.set_etat(EtatLecture())
    
    def pause(self, lecteur):
        print("[ATTENTION] Déjà arrêté")
    
    def stop(self, lecteur):
        print("[ATTENTION] Déjà arrêté")

class EtatLecture(EtatLecteur):
    """État : En lecture"""
    
    def play(self, lecteur):
        print("[ATTENTION] Déjà en lecture")
    
    def pause(self, lecteur: LecteurAudio):
        print("[DOUBLE_VERTICAL_BAR] Lecture en pause")
        lecteur.set_etat(EtatPause())
    
    def stop(self, lecteur: LecteurAudio):
        print("[BLACK_SQUARE_FOR_STOP] Lecture arrêtée")
        lecteur.set_etat(EtatArrete())

class EtatPause(EtatLecteur):
    """État : En pause"""
    
    def play(self, lecteur: LecteurAudio):
        print("[BLACK_RIGHT-POINTING_TRIANGLE] Reprise de la lecture")
        lecteur.set_etat(EtatLecture())
    
    def pause(self, lecteur):
        print("[ATTENTION] Déjà en pause")
    
    def stop(self, lecteur: LecteurAudio):
        print("[BLACK_SQUARE_FOR_STOP] Arrêt")
        lecteur.set_etat(EtatArrete())

# Test
print("\n=== LECTEUR AUDIO ===\n")

lecteur = LecteurAudio()
lecteur.charger_piste("chanson.mp3")

print("\nAction: Play")
lecteur.play()

print("\nAction: Pause")
lecteur.pause()

print("\nAction: Play (reprise)")
lecteur.play()

print("\nAction: Stop")
lecteur.stop()

print("\nAction: Pause (impossible)")
lecteur.pause()


# ----------------------------------------------------------------------------
# [LISTE] PATTERN TEMPLATE METHOD
# ----------------------------------------------------------------------------

"""
TEMPLATE METHOD = Définir squelette d'algorithme, sous-classes redéfinissent étapes

[IDEE] Analogie :
Recette de cuisine
- Étapes communes : préchauffer four, servir
- Étapes variables : préparation ingrédients
Squelette identique, détails différents


CAS D'USAGE :
- Frameworks
- Traitement de données
- Tests unitaires (setUp/tearDown)
- Algorithmes avec variations


PROBLÈME : CODE DUPLIQUÉ DANS SOUS-CLASSES
"""

# [X] Sans Template Method

class RapportPDF:
    def generer(self):
        print("Collecte données")
        print("Format PDF")
        print("Ajout en-tête PDF")
        print("Ajout contenu")
        print("Ajout pied de page PDF")
        print("Sauvegarde PDF")

class RapportHTML:
    def generer(self):
        print("Collecte données")  # <- Dupliqué
        print("Format HTML")
        print("Ajout en-tête HTML")
        print("Ajout contenu")  # <- Dupliqué
        print("Ajout pied de page HTML")
        print("Sauvegarde HTML")

"""
SOLUTION : TEMPLATE METHOD PATTERN
"""

from abc import ABC, abstractmethod

class GenerateurRapport(ABC):
    """
    [IDEE] TEMPLATE METHOD
    
    Définit squelette de l'algorithme
    """
    
    def generer_rapport(self):
        """
        Template method : squelette de l'algorithme
        
        Étapes fixes ET variables
        """
        print(f"\n[GRAPHIQUE] Génération rapport {self.__class__.__name__}\n")
        
        # Étapes communes
        self.collecter_donnees()
        
        # Étapes variables (hook methods)
        self.formater_rapport()
        self.ajouter_entete()
        self.ajouter_contenu()
        self.ajouter_pied_de_page()
        
        # Étape commune
        self.sauvegarder()
        
        print(f"\n[OK] Rapport généré\n")
    
    # Méthodes concrètes (communes)
    def collecter_donnees(self):
        print("  [ENTREE] Collecte des données")
    
    def sauvegarder(self):
        print(f"  [SAUVEGARDE] Sauvegarde du rapport")
    
    # Méthodes abstraites (à implémenter)
    @abstractmethod
    def formater_rapport(self):
        pass
    
    @abstractmethod
    def ajouter_entete(self):
        pass
    
    @abstractmethod
    def ajouter_contenu(self):
        pass
    
    @abstractmethod
    def ajouter_pied_de_page(self):
        pass

# Implémentations concrètes
class RapportPDF(GenerateurRapport):
    """Rapport PDF"""
    
    def formater_rapport(self):
        print("  [FICHIER] Format : PDF")
    
    def ajouter_entete(self):
        print("  [IMPORTANT] En-tête PDF ajouté")
    
    def ajouter_contenu(self):
        print("  [NOTE] Contenu PDF ajouté")
    
    def ajouter_pied_de_page(self):
        print("  [IMPORTANT] Pied de page PDF ajouté")

class RapportHTML(GenerateurRapport):
    """Rapport HTML"""
    
    def formater_rapport(self):
        print("  [WEB] Format : HTML")
    
    def ajouter_entete(self):
        print("  [IMPORTANT] <header> ajouté")
    
    def ajouter_contenu(self):
        print("  [NOTE] <body> ajouté")
    
    def ajouter_pied_de_page(self):
        print("  [IMPORTANT] <footer> ajouté")

class RapportCSV(GenerateurRapport):
    """Rapport CSV"""
    
    def formater_rapport(self):
        print("  [GRAPHIQUE] Format : CSV")
    
    def ajouter_entete(self):
        print("  [IMPORTANT] En-têtes colonnes ajoutés")
    
    def ajouter_contenu(self):
        print("  [NOTE] Lignes de données ajoutées")
    
    def ajouter_pied_de_page(self):
        # Optionnel pour CSV
        pass

# Utilisation
rapports = [
    RapportPDF(),
    RapportHTML(),
    RapportCSV()
]

for rapport in rapports:
    rapport.generer_rapport()

"""
TEMPLATE METHOD AVEC HOOKS
"""

class JeuDePlateau(ABC):
    """Template pour jeux de plateau"""
    
    def jouer(self):
        """Template method"""
        self.initialiser_jeu()
        
        while not self.est_termine():
            self.faire_tour()
        
        self.afficher_gagnant()
        
        # Hook optionnel
        if self.nettoyer_necessaire():
            self.nettoyer()
    
    # Méthodes abstraites (obligatoires)
    @abstractmethod
    def initialiser_jeu(self):
        pass
    
    @abstractmethod
    def faire_tour(self):
        pass
    
    @abstractmethod
    def est_termine(self):
        pass
    
    @abstractmethod
    def afficher_gagnant(self):
        pass
    
    # Hook method (optionnel, comportement par défaut)
    def nettoyer_necessaire(self):
        """Hook : peut être redéfini"""
        return False
    
    def nettoyer(self):
        """Hook : peut être redéfini"""
        print("  [NETTOYAGE] Nettoyage...")


# Ce fichier continue...
# ============================================================================

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 4 (FIN) : PATTERNS COMPORTEMENTAUX (SUITE)
# ============================================================================
#
# [OBJECTIF] CONTENU :
# - Fin Chapitre 17 : Iterator et Chain of Responsibility
# - Exercices pratiques patterns comportementaux
# - Récapitulatif complet des patterns
#
# [TEMPS] TEMPS : ~2-3 heures
# [DOCS] PRÉREQUIS : Début Chapitre 17
# ============================================================================


# Suite du Chapitre 17...


# ----------------------------------------------------------------------------
# [SYNC] PATTERN ITERATOR
# ----------------------------------------------------------------------------

"""
ITERATOR = Parcourir collection sans exposer sa structure interne

[IDEE] Analogie :
Télécommande TV
- Bouton "suivant" : passe au canal suivant
- Pas besoin de connaître structure interne
- Parcours séquentiel simple


CAS D'USAGE :
- Collections personnalisées
- Parcours complexes
- Lazy loading de données
- Streaming


[ATTENTION] Python a déjà des itérateurs intégrés !
Mais comprendre le pattern aide à créer les vôtres.


RAPPEL : ITÉRATEURS EN PYTHON
"""

# Python intégré
ma_liste = [1, 2, 3, 4, 5]

# for utilise __iter__ et __next__
for element in ma_liste:
    print(element)

# Équivalent manuel
iterateur = iter(ma_liste)  # Appelle __iter__()
while True:
    try:
        element = next(iterateur)  # Appelle __next__()
        print(element)
    except StopIteration:
        break

"""
CRÉER ITÉRATEUR PERSONNALISÉ
"""

class Compteur:
    """
    [IDEE] ITERATOR PERSONNALISÉ
    
    Compte de start à end
    """
    
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.current = start
    
    def __iter__(self):
        """Retourne l'itérateur (self)"""
        return self
    
    def __next__(self):
        """Retourne élément suivant"""
        if self.current > self.end:
            raise StopIteration
        
        value = self.current
        self.current += 1
        return value

# Utilisation
print("\n=== COMPTEUR PERSONNALISÉ ===")
for nombre in Compteur(1, 5):
    print(nombre)

"""
ITÉRATEUR VS ITÉRABLE (RÉUTILISABLE)
"""

class CompteurReutilisable:
    """
    [IDEE] ITERABLE (pas Itérateur)
    
    Peut créer plusieurs itérateurs
    """
    
    def __init__(self, start, end):
        self.start = start
        self.end = end
    
    def __iter__(self):
        """Retourne NOUVEL itérateur à chaque fois"""
        return CompteurIterateur(self.start, self.end)

class CompteurIterateur:
    """Itérateur pour CompteurReutilisable"""
    
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current > self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

# Test réutilisabilité
compteur = CompteurReutilisable(1, 3)

print("\nPremière itération:")
for i in compteur:
    print(i)

print("\nDeuxième itération (fonctionne!):")
for i in compteur:
    print(i)

"""
EXEMPLE COMPLET : ITÉRATEUR D'ARBRE BINAIRE
"""

class Noeud:
    """Nœud d'arbre binaire"""
    
    def __init__(self, valeur):
        self.valeur = valeur
        self.gauche = None
        self.droit = None

class ArbreBinaire:
    """Arbre binaire avec itérateur"""
    
    def __init__(self, racine: Noeud):
        self.racine = racine
    
    def __iter__(self):
        """Parcours en ordre (in-order)"""
        return IterateurInOrder(self.racine)

class IterateurInOrder:
    """
    [IDEE] ITERATOR POUR ARBRE
    
    Parcours in-order : gauche -> racine -> droit
    """
    
    def __init__(self, racine: Noeud):
        self.pile = []
        self._push_gauche(racine)
    
    def _push_gauche(self, noeud):
        """Empiler tous les nœuds gauches"""
        while noeud:
            self.pile.append(noeud)
            noeud = noeud.gauche
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if not self.pile:
            raise StopIteration
        
        # Dépiler nœud
        noeud = self.pile.pop()
        
        # Empiler sous-arbre droit
        if noeud.droit:
            self._push_gauche(noeud.droit)
        
        return noeud.valeur

# Construction arbre
#       4
#      / \
#     2   6
#    / \ / \
#   1  3 5  7

racine = Noeud(4)
racine.gauche = Noeud(2)
racine.droit = Noeud(6)
racine.gauche.gauche = Noeud(1)
racine.gauche.droit = Noeud(3)
racine.droit.gauche = Noeud(5)
racine.droit.droit = Noeud(7)

# Parcours
arbre = ArbreBinaire(racine)

print("\n=== PARCOURS ARBRE (IN-ORDER) ===")
for valeur in arbre:
    print(valeur, end=" ")
print()  # 1 2 3 4 5 6 7

"""
ITÉRATEUR LAZY (GÉNÉRATEUR)
"""

class FibonacciIterator:
    """Itérateur Fibonacci (infini)"""
    
    def __init__(self, max_count=None):
        self.max_count = max_count
        self.count = 0
        self.a = 0
        self.b = 1
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.max_count and self.count >= self.max_count:
            raise StopIteration
        
        self.count += 1
        result = self.a
        self.a, self.b = self.b, self.a + self.b
        return result

# Limité
print("\n=== FIBONACCI (10 premiers) ===")
for nombre in FibonacciIterator(10):
    print(nombre, end=" ")
print()

# Avec générateur (plus pythonique)
def fibonacci(max_count=None):
    """Générateur Fibonacci"""
    a, b = 0, 1
    count = 0
    
    while max_count is None or count < max_count:
        yield a
        a, b = b, a + b
        count += 1

print("\n=== FIBONACCI (générateur) ===")
for nombre in fibonacci(10):
    print(nombre, end=" ")
print()


# ----------------------------------------------------------------------------
# [LIEN] PATTERN CHAIN OF RESPONSIBILITY
# ----------------------------------------------------------------------------

"""
CHAIN OF RESPONSIBILITY = Chaîne de handlers qui traitent requête séquentiellement

[IDEE] Analogie :
Service client
- Niveau 1 : Questions simples
- Niveau 2 : Questions techniques
- Niveau 3 : Questions complexes
- Manager : Cas exceptionnels

Requête passe de niveau en niveau jusqu'à résolution


CAS D'USAGE :
- Système de logging (niveaux)
- Middleware web
- Validation en cascade
- Traitement d'événements


PROBLÈME : IF/ELSE EN CASCADE
"""

# [X] Sans Chain of Responsibility

def traiter_requete(requete):
    if requete['type'] == 'simple':
        print("Support niveau 1 traite")
    elif requete['type'] == 'technique':
        print("Support niveau 2 traite")
    elif requete['type'] == 'complexe':
        print("Support niveau 3 traite")
    elif requete['type'] == 'exceptionnel':
        print("Manager traite")
    else:
        print("Non traité")
    # [X] Rigide, difficile d'ajouter niveaux

"""
SOLUTION : CHAIN OF RESPONSIBILITY PATTERN
"""

from abc import ABC, abstractmethod

class Handler(ABC):
    """
    [IDEE] HANDLER (Interface)
    
    Chaînon de la chaîne
    """
    
    def __init__(self):
        self._next_handler = None
    
    def set_next(self, handler):
        """Définir handler suivant"""
        self._next_handler = handler
        return handler  # Permet chaînage fluide
    
    def handle(self, requete):
        """
        Traiter ou passer au suivant
        
        Template method pattern !
        """
        if self.can_handle(requete):
            return self.process(requete)
        elif self._next_handler:
            return self._next_handler.handle(requete)
        else:
            return "[X] Aucun handler disponible"
    
    @abstractmethod
    def can_handle(self, requete):
        """Peut traiter cette requête ?"""
        pass
    
    @abstractmethod
    def process(self, requete):
        """Traiter la requête"""
        pass

# Handlers concrets
class SupportNiveau1(Handler):
    """Support : Questions simples"""
    
    def can_handle(self, requete):
        return requete.get('complexite') == 1
    
    def process(self, requete):
        return f"[OK] Support N1: '{requete['question']}' résolu"

class SupportNiveau2(Handler):
    """Support : Questions techniques"""
    
    def can_handle(self, requete):
        return requete.get('complexite') == 2
    
    def process(self, requete):
        return f"[OK] Support N2: '{requete['question']}' résolu (technique)"

class SupportNiveau3(Handler):
    """Support : Questions complexes"""
    
    def can_handle(self, requete):
        return requete.get('complexite') == 3
    
    def process(self, requete):
        return f"[OK] Support N3: '{requete['question']}' résolu (expert)"

class Manager(Handler):
    """Manager : Cas exceptionnels"""
    
    def can_handle(self, requete):
        return True  # Traite tout ce qui reste
    
    def process(self, requete):
        return f"[OK] Manager: '{requete['question']}' pris en charge"

# Construction de la chaîne
print("\n=== CHAÎNE DE SUPPORT ===\n")

niveau1 = SupportNiveau1()
niveau2 = SupportNiveau2()
niveau3 = SupportNiveau3()
manager = Manager()

# Chaînage fluide
niveau1.set_next(niveau2).set_next(niveau3).set_next(manager)

# Requêtes
requetes = [
    {'question': 'Comment me connecter?', 'complexite': 1},
    {'question': 'Erreur 500 serveur', 'complexite': 2},
    {'question': 'Optimisation algorithme', 'complexite': 3},
    {'question': 'Demande exceptionnelle', 'complexite': 4}
]

for req in requetes:
    resultat = niveau1.handle(req)  # Commence toujours par le premier
    print(resultat)

"""
EXEMPLE COMPLET : SYSTÈME DE LOGGING
"""

import datetime

class LogLevel:
    """Niveaux de log"""
    DEBUG = 1
    INFO = 2
    WARNING = 3
    ERROR = 4
    CRITICAL = 5

class Logger(ABC):
    """Handler de base pour logging"""
    
    def __init__(self, level):
        self.level = level
        self._next_logger = None
    
    def set_next(self, logger):
        self._next_logger = logger
        return logger
    
    def log(self, level, message):
        """Traiter message selon niveau"""
        if level >= self.level:
            self.write(level, message)
        
        # Toujours passer au suivant (comportement différent de Chain classique)
        if self._next_logger:
            self._next_logger.log(level, message)
    
    @abstractmethod
    def write(self, level, message):
        """Écrire le log"""
        pass

class ConsoleLogger(Logger):
    """Logger : Console"""
    
    def write(self, level, message):
        timestamp = datetime.datetime.now().strftime("%H:%M:%S")
        level_name = {
            1: "DEBUG", 2: "INFO", 3: "WARNING", 4: "ERROR", 5: "CRITICAL"
        }.get(level, "UNKNOWN")
        print(f"[CONSOLE] {timestamp} {level_name}: {message}")

class FileLogger(Logger):
    """Logger : Fichier"""
    
    def __init__(self, level, filename="app.log"):
        super().__init__(level)
        self.filename = filename
    
    def write(self, level, message):
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        level_name = {
            1: "DEBUG", 2: "INFO", 3: "WARNING", 4: "ERROR", 5: "CRITICAL"
        }.get(level, "UNKNOWN")
        print(f"[FILE:{self.filename}] {timestamp} {level_name}: {message}")

class EmailLogger(Logger):
    """Logger : Email (erreurs critiques)"""
    
    def __init__(self, email):
        super().__init__(LogLevel.ERROR)  # Seulement erreurs
        self.email = email
    
    def write(self, level, message):
        print(f"[EMAIL:{self.email}] [ALERTE] ERREUR: {message}")

# Construction chaîne
print("\n=== SYSTÈME DE LOGGING ===\n")

console = ConsoleLogger(LogLevel.DEBUG)  # Tout
fichier = FileLogger(LogLevel.INFO)       # INFO et plus
email = EmailLogger("admin@example.com")  # ERROR et plus

console.set_next(fichier).set_next(email)

# Tests
console.log(LogLevel.DEBUG, "Message de debug")
console.log(LogLevel.INFO, "Information générale")
console.log(LogLevel.WARNING, "Attention!")
console.log(LogLevel.ERROR, "Erreur détectée!")
console.log(LogLevel.CRITICAL, "Erreur critique!")

"""
CHAIN OF RESPONSIBILITY : VALIDATION EN CASCADE
"""

class Validator(ABC):
    """Validateur de base"""
    
    def __init__(self):
        self._next_validator = None
    
    def set_next(self, validator):
        self._next_validator = validator
        return validator
    
    def validate(self, data):
        """Valider et passer au suivant si OK"""
        if not self.is_valid(data):
            return False, self.get_error_message(data)
        
        # Validation OK, passer au suivant
        if self._next_validator:
            return self._next_validator.validate(data)
        
        return True, "Validation réussie"
    
    @abstractmethod
    def is_valid(self, data):
        pass
    
    @abstractmethod
    def get_error_message(self, data):
        pass

class EmailValidator(Validator):
    """Validateur : Email"""
    
    def is_valid(self, data):
        email = data.get('email', '')
        return '@' in email and '.' in email
    
    def get_error_message(self, data):
        return "Email invalide"

class AgeValidator(Validator):
    """Validateur : Âge"""
    
    def is_valid(self, data):
        age = data.get('age', 0)
        return 18 <= age <= 100
    
    def get_error_message(self, data):
        return "Âge doit être entre 18 et 100"

class PasswordValidator(Validator):
    """Validateur : Mot de passe"""
    
    def is_valid(self, data):
        password = data.get('password', '')
        return (len(password) >= 8 and 
                any(c.isupper() for c in password) and
                any(c.isdigit() for c in password))
    
    def get_error_message(self, data):
        return "Mot de passe faible (min 8 chars, 1 majuscule, 1 chiffre)"

# Chaîne de validation
print("\n=== VALIDATION EN CASCADE ===\n")

email_val = EmailValidator()
age_val = AgeValidator()
password_val = PasswordValidator()

email_val.set_next(age_val).set_next(password_val)

# Tests
utilisateurs = [
    {
        'email': 'alice@example.com',
        'age': 25,
        'password': 'SecurePass123'
    },
    {
        'email': 'bob.example.com',  # [X] Email invalide
        'age': 30,
        'password': 'StrongPass1'
    },
    {
        'email': 'charlie@example.com',
        'age': 15,  # [X] Âge invalide
        'password': 'GoodPass123'
    },
    {
        'email': 'david@example.com',
        'age': 28,
        'password': 'weak'  # [X] Mot de passe faible
    }
]

for user in utilisateurs:
    valid, message = email_val.validate(user)
    status = "[OK]" if valid else "[X]"
    print(f"{status} {user['email']}: {message}")


# ============================================================================
# [DOCS] RÉCAPITULATIF COMPLET DES DESIGN PATTERNS
# ============================================================================

"""
[BRAVO] FÉLICITATIONS ! TOUS LES PATTERNS MAÎTRISÉS !

═══════════════════════════════════════════════════════════════════════════
[PACKAGE] PATTERNS CRÉATIONNELS (Création d'objets)
═══════════════════════════════════════════════════════════════════════════

┌─────────────────┬─────────────────────────────────────────────────────┐
│ PATTERN         │ USAGE                                               │
├─────────────────┼─────────────────────────────────────────────────────┤
│ Singleton       │ Une seule instance (config, logger, pool)          │
│ Factory Method  │ Création flexible via méthode                       │
│ Abstract Factory│ Familles d'objets cohérents (UI, thèmes)          │
│ Builder         │ Construction étape par étape (objets complexes)    │
│ Prototype       │ Clonage d'objets (éviter création coûteuse)       │
└─────────────────┴─────────────────────────────────────────────────────┘


═══════════════════════════════════════════════════════════════════════════
[CONSTRUCTION] PATTERNS STRUCTURELS (Organisation de classes)
═══════════════════════════════════════════════════════════════════════════

┌─────────────────┬─────────────────────────────────────────────────────┐
│ PATTERN         │ USAGE                                               │
├─────────────────┼─────────────────────────────────────────────────────┤
│ Adapter         │ Compatibilité d'interfaces (wrapper API)           │
│ Decorator       │ Ajout dynamique fonctionnalités (logging, cache)   │
│ Facade          │ Simplification système complexe (API unifiée)      │
│ Proxy           │ Contrôle d'accès (lazy load, protection, cache)    │
│ Composite       │ Structures arborescentes (fichiers, menus, UI)     │
└─────────────────┴─────────────────────────────────────────────────────┘


═══════════════════════════════════════════════════════════════════════════
[SCENARIO] PATTERNS COMPORTEMENTAUX (Interactions entre objets)
═══════════════════════════════════════════════════════════════════════════

┌─────────────────────────┬───────────────────────────────────────────────┐
│ PATTERN                 │ USAGE                                         │
├─────────────────────────┼───────────────────────────────────────────────┤
│ Observer                │ Pub/Sub, événements, notifications            │
│ Strategy                │ Algorithmes interchangeables                  │
│ Command                 │ Encapsulation actions, undo/redo              │
│ State                   │ Comportement selon état                       │
│ Template Method         │ Squelette algorithme, étapes variables        │
│ Iterator                │ Parcours de collections                       │
│ Chain of Responsibility │ Chaîne de traitement (logging, validation)    │
└─────────────────────────┴───────────────────────────────────────────────┘


═══════════════════════════════════════════════════════════════════════════
[OBJECTIF] QUAND UTILISER QUEL PATTERN ?
═══════════════════════════════════════════════════════════════════════════

PROBLÈME                              -> PATTERN
────────────────────────────────────────────────────────────────────────────
Une seule instance nécessaire         -> Singleton
Créer objets sans connaître type      -> Factory Method
Familles d'objets cohérents          -> Abstract Factory
Objet complexe à construire          -> Builder
Cloner objets existants              -> Prototype

Interfaces incompatibles             -> Adapter
Ajouter fonctionnalités dynamique    -> Decorator
Simplifier système complexe          -> Facade
Contrôler accès objet                -> Proxy
Structure arborescente               -> Composite

Notifier plusieurs objets            -> Observer
Changer algorithme à runtime         -> Strategy
Annuler actions (undo)               -> Command
Comportement change selon état       -> State
Squelette avec étapes variables      -> Template Method
Parcourir collection custom          -> Iterator
Chaîne de traitement                 -> Chain of Responsibility


═══════════════════════════════════════════════════════════════════════════
[IDEE] PRINCIPES CLÉS DES DESIGN PATTERNS
═══════════════════════════════════════════════════════════════════════════

1. DÉCOUPLAGE
   -> Réduire dépendances entre classes
   -> Interfaces, abstractions

2. RÉUTILISATION
   -> Code réutilisable
   -> Composition > Héritage

3. FLEXIBILITÉ
   -> Facile d'étendre
   -> Ouvert/Fermé

4. RESPONSABILITÉ UNIQUE
   -> Une classe = une responsabilité

5. INVERSION DE DÉPENDANCE
   -> Dépendre d'abstractions


═══════════════════════════════════════════════════════════════════════════
[ATTENTION] PIÈGES À ÉVITER
═══════════════════════════════════════════════════════════════════════════

[X] Utiliser pattern sans raison
   -> YAGNI (You Ain't Gonna Need It)

[X] Over-engineering
   -> Simple > Complexe

[X] Pattern pour le pattern
   -> Résoudre VRAI problème

[OK] Utiliser quand :
   - Problème récurrent identifié
   - Code plus simple avec pattern
   - Maintenabilité améliorée


═══════════════════════════════════════════════════════════════════════════
[COURS] COMPÉTENCES ACQUISES
═══════════════════════════════════════════════════════════════════════════

Vous maîtrisez maintenant :

[OK] 15+ design patterns essentiels
[OK] Quand et pourquoi utiliser chaque pattern
[OK] Implémentation en Python
[OK] Combinaison de patterns
[OK] Principes de conception orientée objet
[OK] Architecture logicielle solide


═══════════════════════════════════════════════════════════════════════════
-> PROCHAINE ÉTAPE : PRINCIPES SOLID ET BEST PRACTICES
═══════════════════════════════════════════════════════════════════════════

La suite couvrira :

PRINCIPES SOLID :
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle

BEST PRACTICES :
- Testing orienté objet
- Documentation (docstrings, type hints)
- Code review
- Performance
- Anti-patterns à éviter


[GUIDE] Prochains fichiers :
- poo_partie4_principes_solid.txt
- poo_partie4_best_practices.txt
- poo_partie4_testing.txt


═══════════════════════════════════════════════════════════════════════════
[TROPHEE] TABLEAU DE MAÎTRISE
═══════════════════════════════════════════════════════════════════════════

Partie 1 : Fondamentaux                    [OK] 100%
Partie 2 : Héritage & Polymorphisme        [OK] 100%
Partie 3 : Concepts Avancés                [OK] 100%
Partie 4 : Design Patterns                 [OK] 100%
  ├─ Créationnels                          [OK] 100%
  ├─ Structurels                           [OK] 100%
  └─ Comportementaux                       [OK] 100%

SOLID & Best Practices                     [HOURGLASS_WITH_FLOWING_SAND] À venir


Excellent travail ! Vous êtes maintenant un expert POO Python ! [RAPIDE]

Prêt pour SOLID et les best practices ? Continuez ! [FORCE]
"""

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 4 (SUITE) : PRINCIPES SOLID
# ============================================================================
#
# [OBJECTIF] CONTENU :
# - Chapitre 18 : Les 5 Principes SOLID
#   * Single Responsibility Principle (SRP)
#   * Open/Closed Principle (OCP)
#   * Liskov Substitution Principle (LSP)
#   * Interface Segregation Principle (ISP)
#   * Dependency Inversion Principle (DIP)
#
# [TEMPS] TEMPS : ~5-6 heures
# [DOCS] PRÉREQUIS : Design Patterns (Chapitres 14-17)
# [IDEE] NIVEAU : Avancé - Principes fondamentaux d'architecture
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 18 : PRINCIPES SOLID
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce que SOLID et pourquoi c'est important
[OK] Appliquer Single Responsibility Principle
[OK] Concevoir code Open/Closed
[OK] Respecter Liskov Substitution Principle
[OK] Séparer interfaces (ISP)
[OK] Inverser dépendances (DIP)
[OK] Reconnaître violations de SOLID
[OK] Refactorer code pour respecter SOLID
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] INTRODUCTION À SOLID
# ----------------------------------------------------------------------------

"""
QU'EST-CE QUE SOLID ?

SOLID = 5 principes de conception orientée objet
Acronyme créé par Robert C. Martin ("Uncle Bob")

S - Single Responsibility Principle
O - Open/Closed Principle
L - Liskov Substitution Principle
I - Interface Segregation Principle
D - Dependency Inversion Principle


[IDEE] POURQUOI SOLID ?

[OK] Code maintenable
   Modifications localisées, pas d'effet domino

[OK] Code testable
   Classes indépendantes, facile à mocker

[OK] Code réutilisable
   Composants découplés

[OK] Code compréhensible
   Responsabilités claires

[OK] Code extensible
   Ajouter fonctionnalités sans casser l'existant


[ATTENTION] SOLID N'EST PAS :

[X] Des règles absolues
   Principes directeurs, pas dogmes

[X] À appliquer partout
   Pragmatisme > Purisme

[X] Complexité gratuite
   Simplicité d'abord


[CLE] RÈGLE D'OR :

Appliquer SOLID quand ça simplifie le code,
pas quand ça le complique !
"""


# ----------------------------------------------------------------------------
# 1⃣ SINGLE RESPONSIBILITY PRINCIPLE (SRP)
# ----------------------------------------------------------------------------

"""
═══════════════════════════════════════════════════════════════════════════
PRINCIPE 1 : SINGLE RESPONSIBILITY PRINCIPLE (SRP)
═══════════════════════════════════════════════════════════════════════════

DÉFINITION :
"Une classe ne devrait avoir qu'UNE SEULE RAISON de changer"

[IDEE] Reformulation :
Une classe = UNE responsabilité
Une classe = UN acteur métier

Chaque classe doit avoir un seul travail bien défini


POURQUOI ?

[OK] Facilite compréhension
[OK] Facilite tests
[OK] Réduit couplage
[OK] Facilite modifications


PROBLÈME : VIOLATION DE SRP
"""

# [X] MAUVAIS : Classe avec trop de responsabilités

class Utilisateur:
    """
    [X] VIOLATION SRP
    
    Cette classe a PLUSIEURS responsabilités :
    1. Gestion des données utilisateur
    2. Validation
    3. Persistance en base de données
    4. Envoi d'emails
    5. Génération de rapports
    
    -> 5 raisons de changer !
    """
    
    def __init__(self, nom, email, mot_de_passe):
        self.nom = nom
        self.email = email
        self.mot_de_passe = mot_de_passe
    
    # Responsabilité 1 : Validation
    def valider_email(self):
        return '@' in self.email
    
    def valider_mot_de_passe(self):
        return len(self.mot_de_passe) >= 8
    
    # Responsabilité 2 : Persistance
    def sauvegarder_en_db(self):
        print(f"INSERT INTO users VALUES ({self.nom}, {self.email})")
    
    def charger_depuis_db(self, user_id):
        print(f"SELECT * FROM users WHERE id = {user_id}")
    
    # Responsabilité 3 : Communication
    def envoyer_email_bienvenue(self):
        print(f"Email envoyé à {self.email}")
    
    # Responsabilité 4 : Rapports
    def generer_rapport_activite(self):
        print(f"Rapport pour {self.nom}")

"""
Problèmes :
- Changement validation -> modifier Utilisateur
- Changement DB -> modifier Utilisateur
- Changement email -> modifier Utilisateur
- Changement rapports -> modifier Utilisateur
- Difficile à tester (dépendances multiples)
- Réutilisation impossible (tout ou rien)
"""


# [OK] SOLUTION : SÉPARER LES RESPONSABILITÉS

class Utilisateur:
    """
    [OK] RESPECTE SRP
    
    Une seule responsabilité : Représenter un utilisateur
    """
    
    def __init__(self, nom, email, mot_de_passe):
        self.nom = nom
        self.email = email
        self.mot_de_passe = mot_de_passe
    
    def __repr__(self):
        return f"Utilisateur(nom={self.nom!r}, email={self.email!r})"


class ValidateurUtilisateur:
    """
    [OK] RESPECTE SRP
    
    Une seule responsabilité : Valider utilisateur
    """
    
    @staticmethod
    def valider_email(email: str) -> bool:
        """Valider format email"""
        return '@' in email and '.' in email
    
    @staticmethod
    def valider_mot_de_passe(mot_de_passe: str) -> bool:
        """Valider robustesse mot de passe"""
        return (len(mot_de_passe) >= 8 and
                any(c.isupper() for c in mot_de_passe) and
                any(c.isdigit() for c in mot_de_passe))
    
    @classmethod
    def valider_utilisateur(cls, utilisateur: Utilisateur) -> tuple[bool, str]:
        """Valider utilisateur complet"""
        if not cls.valider_email(utilisateur.email):
            return False, "Email invalide"
        
        if not cls.valider_mot_de_passe(utilisateur.mot_de_passe):
            return False, "Mot de passe faible"
        
        return True, "Validation OK"


class UtilisateurRepository:
    """
    [OK] RESPECTE SRP
    
    Une seule responsabilité : Persistance utilisateur
    """
    
    def sauvegarder(self, utilisateur: Utilisateur):
        """Sauvegarder en base de données"""
        print(f"INSERT INTO users VALUES ('{utilisateur.nom}', '{utilisateur.email}')")
    
    def charger(self, user_id: int) -> Utilisateur:
        """Charger depuis base de données"""
        print(f"SELECT * FROM users WHERE id = {user_id}")
        # Simulé
        return Utilisateur("John", "john@example.com", "Pass123")
    
    def supprimer(self, user_id: int):
        """Supprimer de la base"""
        print(f"DELETE FROM users WHERE id = {user_id}")


class ServiceEmail:
    """
    [OK] RESPECTE SRP
    
    Une seule responsabilité : Envoi d'emails
    """
    
    def envoyer_bienvenue(self, utilisateur: Utilisateur):
        """Envoyer email de bienvenue"""
        print(f"[EMAIL] Email bienvenue envoyé à {utilisateur.email}")
    
    def envoyer_reset_password(self, utilisateur: Utilisateur):
        """Envoyer email reset mot de passe"""
        print(f"[EMAIL] Email reset envoyé à {utilisateur.email}")


class GenerateurRapport:
    """
    [OK] RESPECTE SRP
    
    Une seule responsabilité : Génération de rapports
    """
    
    def generer_rapport_utilisateur(self, utilisateur: Utilisateur):
        """Générer rapport d'activité"""
        print(f"[GRAPHIQUE] Rapport pour {utilisateur.nom}")
        return f"=== Rapport ===\nNom: {utilisateur.nom}\nEmail: {utilisateur.email}"


# Utilisation avec SRP
print("=== UTILISATION AVEC SRP ===\n")

# Créer utilisateur
user = Utilisateur("Alice", "alice@example.com", "SecurePass123")

# Valider
validateur = ValidateurUtilisateur()
est_valide, message = validateur.valider_utilisateur(user)
print(f"Validation: {message}")

# Sauvegarder
if est_valide:
    repo = UtilisateurRepository()
    repo.sauvegarder(user)
    
    # Notifier
    email_service = ServiceEmail()
    email_service.envoyer_bienvenue(user)
    
    # Rapport
    generateur = GenerateurRapport()
    rapport = generateur.generer_rapport_utilisateur(user)
    print(f"\n{rapport}")

"""
[OK] AVANTAGES :

1. MODIFICATION LOCALISÉE
   Changement validation -> modifier ValidateurUtilisateur uniquement

2. TESTABILITÉ
   Tester chaque classe indépendamment

3. RÉUTILISABILITÉ
   ValidateurUtilisateur utilisable ailleurs

4. COMPRÉHENSION
   Chaque classe a un nom et rôle clairs


EXEMPLE COMPLET : SYSTÈME DE FACTURATION
"""

# [X] VIOLATION SRP

class Facture:
    """[X] Trop de responsabilités"""
    
    def __init__(self, numero, client, articles):
        self.numero = numero
        self.client = client
        self.articles = articles
    
    def calculer_total(self):
        """Responsabilité 1 : Calcul"""
        return sum(a['prix'] * a['quantite'] for a in self.articles)
    
    def generer_pdf(self):
        """Responsabilité 2 : Génération PDF"""
        print(f"PDF généré pour facture {self.numero}")
    
    def envoyer_par_email(self):
        """Responsabilité 3 : Envoi email"""
        print(f"Facture {self.numero} envoyée par email")
    
    def sauvegarder_en_db(self):
        """Responsabilité 4 : Persistance"""
        print(f"Facture {self.numero} sauvegardée")


# [OK] RESPECTE SRP

class Facture:
    """[OK] Seulement données et logique métier"""
    
    def __init__(self, numero, client, articles):
        self.numero = numero
        self.client = client
        self.articles = articles
    
    def calculer_total(self):
        """Calcul métier (fait partie de Facture)"""
        return sum(a['prix'] * a['quantite'] for a in self.articles)
    
    def __repr__(self):
        return f"Facture({self.numero}, {self.client}, total={self.calculer_total()}€)"


class GenerateurPDFFacture:
    """[OK] Responsabilité : Génération PDF"""
    
    def generer(self, facture: Facture) -> bytes:
        """Générer PDF de la facture"""
        print(f"[FICHIER] Génération PDF facture {facture.numero}")
        # Simulation
        contenu = f"""
        FACTURE N°{facture.numero}
        Client: {facture.client}
        Total: {facture.calculer_total()}€
        """
        return contenu.encode()


class ServiceEnvoiFacture:
    """[OK] Responsabilité : Envoi facture"""
    
    def envoyer_par_email(self, facture: Facture, pdf: bytes):
        """Envoyer facture par email"""
        print(f"[EMAIL] Envoi facture {facture.numero} par email")
    
    def envoyer_par_courrier(self, facture: Facture, pdf: bytes):
        """Envoyer facture par courrier"""
        print(f"[POSTBOX] Envoi facture {facture.numero} par courrier")


class FactureRepository:
    """[OK] Responsabilité : Persistance facture"""
    
    def sauvegarder(self, facture: Facture):
        """Sauvegarder en base"""
        print(f"[SAUVEGARDE] Facture {facture.numero} sauvegardée en DB")
    
    def charger(self, numero: str) -> Facture:
        """Charger depuis base"""
        print(f"[ENTREE] Chargement facture {numero}")
        # Simulé
        return Facture(numero, "Client Test", [])


# Orchestration (peut être dans un service)
class ServiceFacturation:
    """Service orchestrant la facturation"""
    
    def __init__(self):
        self.pdf_generator = GenerateurPDFFacture()
        self.email_service = ServiceEnvoiFacture()
        self.repository = FactureRepository()
    
    def traiter_facture(self, facture: Facture):
        """Traiter facture complète"""
        # 1. Sauvegarder
        self.repository.sauvegarder(facture)
        
        # 2. Générer PDF
        pdf = self.pdf_generator.generer(facture)
        
        # 3. Envoyer
        self.email_service.envoyer_par_email(facture, pdf)


# Test
print("\n=== SYSTÈME FACTURATION (SRP) ===\n")

facture = Facture(
    numero="F2024-001",
    client="Entreprise ABC",
    articles=[
        {'nom': 'Service A', 'prix': 100, 'quantite': 2},
        {'nom': 'Service B', 'prix': 150, 'quantite': 1}
    ]
)

service = ServiceFacturation()
service.traiter_facture(facture)

"""
[CLE] COMMENT IDENTIFIER VIOLATION SRP ?

Questions à se poser :
1. Cette classe a-t-elle plusieurs raisons de changer ?
2. Puis-je décrire la classe en une phrase sans "et" ?
3. Y a-t-il plusieurs acteurs métier concernés ?
4. Plusieurs équipes modifieraient cette classe ?

Si OUI à l'une -> Probablement violation SRP
"""


# ----------------------------------------------------------------------------
# 2⃣ OPEN/CLOSED PRINCIPLE (OCP)
# ----------------------------------------------------------------------------

"""
═══════════════════════════════════════════════════════════════════════════
PRINCIPE 2 : OPEN/CLOSED PRINCIPLE (OCP)
═══════════════════════════════════════════════════════════════════════════

DÉFINITION :
"Ouvert à l'extension, fermé à la modification"

[IDEE] Reformulation :
Pouvoir AJOUTER fonctionnalités SANS MODIFIER code existant


POURQUOI ?

[OK] Évite régression (code existant intact)
[OK] Facilite ajout fonctionnalités
[OK] Code stable et testable


COMMENT ?

-> Abstractions (classes abstraites, interfaces)
-> Polymorphisme
-> Composition
-> Design patterns (Strategy, Decorator, etc.)


PROBLÈME : VIOLATION OCP
"""

# [X] MAUVAIS : Modification pour chaque nouveau type

class CalculateurPrix:
    """
    [X] VIOLATION OCP
    
    Ajouter nouveau type de client -> MODIFIER cette classe
    """
    
    def calculer(self, prix: float, type_client: str) -> float:
        if type_client == "normal":
            return prix
        elif type_client == "etudiant":
            return prix * 0.9  # -10%
        elif type_client == "senior":
            return prix * 0.85  # -15%
        elif type_client == "vip":
            return prix * 0.8  # -20%
        # [X] Nouveau type -> modifier ici (risque de bug)
        else:
            return prix

"""
Problèmes :
- Ajouter type "entreprise" -> modifier CalculateurPrix
- Risque de casser types existants
- Tests à refaire complètement
- Violation principe ouvert/fermé
"""


# [OK] SOLUTION : EXTENSION PAR ABSTRACTION

from abc import ABC, abstractmethod

class StrategieRemise(ABC):
    """
    [OK] ABSTRACTION
    
    Ouvert à extension : nouvelles stratégies
    Fermé à modification : CalculateurPrix inchangé
    """
    
    @abstractmethod
    def calculer_prix(self, prix: float) -> float:
        pass


class RemiseNormale(StrategieRemise):
    """Pas de remise"""
    
    def calculer_prix(self, prix: float) -> float:
        return prix


class RemiseEtudiant(StrategieRemise):
    """Remise étudiant : 10%"""
    
    def calculer_prix(self, prix: float) -> float:
        return prix * 0.9


class RemiseSenior(StrategieRemise):
    """Remise senior : 15%"""
    
    def calculer_prix(self, prix: float) -> float:
        return prix * 0.85


class RemiseVIP(StrategieRemise):
    """Remise VIP : 20%"""
    
    def calculer_prix(self, prix: float) -> float:
        return prix * 0.8


class CalculateurPrix:
    """
    [OK] RESPECTE OCP
    
    Fermé à modification : cette classe ne change jamais
    Ouvert à extension : nouvelles stratégies possibles
    """
    
    def __init__(self, strategie: StrategieRemise):
        self.strategie = strategie
    
    def calculer(self, prix: float) -> float:
        return self.strategie.calculer_prix(prix)


# [OK] EXTENSION : Nouveau type SANS modifier code existant

class RemiseEntreprise(StrategieRemise):
    """
    [OK] EXTENSION
    
    Nouveau type ajouté SANS toucher à CalculateurPrix
    """
    
    def __init__(self, pourcentage: float):
        self.pourcentage = pourcentage
    
    def calculer_prix(self, prix: float) -> float:
        return prix * (1 - self.pourcentage / 100)


# Test
print("\n=== OPEN/CLOSED PRINCIPLE ===\n")

prix_base = 100

calculateurs = [
    ("Normal", CalculateurPrix(RemiseNormale())),
    ("Étudiant", CalculateurPrix(RemiseEtudiant())),
    ("Senior", CalculateurPrix(RemiseSenior())),
    ("VIP", CalculateurPrix(RemiseVIP())),
    ("Entreprise -25%", CalculateurPrix(RemiseEntreprise(25)))
]

for nom, calc in calculateurs:
    print(f"{nom}: {calc.calculer(prix_base)}€")


"""
EXEMPLE COMPLET : SYSTÈME DE RAPPORTS
"""

# [X] VIOLATION OCP

class GenerateurRapport:
    """[X] Fermé à extension"""
    
    def generer(self, donnees: dict, format: str):
        if format == "pdf":
            return self._generer_pdf(donnees)
        elif format == "html":
            return self._generer_html(donnees)
        elif format == "csv":
            return self._generer_csv(donnees)
        # [X] Nouveau format -> modifier cette méthode
        
    def _generer_pdf(self, donnees):
        return f"PDF: {donnees}"
    
    def _generer_html(self, donnees):
        return f"<html>{donnees}</html>"
    
    def _generer_csv(self, donnees):
        return f"CSV: {donnees}"


# [OK] RESPECTE OCP

class FormateurRapport(ABC):
    """Interface pour formateurs"""
    
    @abstractmethod
    def formater(self, donnees: dict) -> str:
        pass


class FormateurPDF(FormateurRapport):
    """Formateur PDF"""
    
    def formater(self, donnees: dict) -> str:
        print("  [FICHIER] Génération PDF")
        return f"=== PDF ===\n{donnees}"


class FormateurHTML(FormateurRapport):
    """Formateur HTML"""
    
    def formater(self, donnees: dict) -> str:
        print("  [WEB] Génération HTML")
        html = "<html><body>"
        for cle, valeur in donnees.items():
            html += f"<p><strong>{cle}:</strong> {valeur}</p>"
        html += "</body></html>"
        return html


class FormateurCSV(FormateurRapport):
    """Formateur CSV"""
    
    def formater(self, donnees: dict) -> str:
        print("  [GRAPHIQUE] Génération CSV")
        lignes = ["cle,valeur"]
        lignes.extend(f"{k},{v}" for k, v in donnees.items())
        return "\n".join(lignes)


class GenerateurRapport:
    """
    [OK] Ouvert à extension, fermé à modification
    """
    
    def __init__(self, formateur: FormateurRapport):
        self.formateur = formateur
    
    def generer(self, donnees: dict) -> str:
        return self.formateur.formater(donnees)


# [OK] Extension : Nouveau format JSON

class FormateurJSON(FormateurRapport):
    """
    [OK] EXTENSION sans modification
    """
    
    def formater(self, donnees: dict) -> str:
        import json
        print("  [PACKAGE] Génération JSON")
        return json.dumps(donnees, indent=2)


# Test
print("\n=== GÉNÉRATEUR RAPPORTS (OCP) ===\n")

donnees = {
    'titre': 'Rapport Mensuel',
    'ventes': 150000,
    'clients': 523
}

formateurs = [
    FormateurPDF(),
    FormateurHTML(),
    FormateurCSV(),
    FormateurJSON()
]

for formateur in formateurs:
    generateur = GenerateurRapport(formateur)
    resultat = generateur.generer(donnees)
    print(resultat[:100], "...\n" if len(resultat) > 100 else "\n")


"""
OCP AVEC DÉCORATEUR (EXTENSION DYNAMIQUE)
"""

class NotificationBase:
    """Notification de base"""
    
    def envoyer(self, message: str):
        print(f"[MESSAGE] Message: {message}")


class DecorateurNotification(ABC):
    """Décorateur pour étendre notifications"""
    
    def __init__(self, notification):
        self.notification = notification
    
    @abstractmethod
    def envoyer(self, message: str):
        pass


class NotificationAvecLog(DecorateurNotification):
    """[OK] Extension : Ajoute logging"""
    
    def envoyer(self, message: str):
        print(f"[NOTE] LOG: Envoi notification")
        self.notification.envoyer(message)


class NotificationAvecRetry(DecorateurNotification):
    """[OK] Extension : Ajoute réessai"""
    
    def __init__(self, notification, max_retries=3):
        super().__init__(notification)
        self.max_retries = max_retries
    
    def envoyer(self, message: str):
        print(f"[SYNC] Système de retry ({self.max_retries} tentatives)")
        self.notification.envoyer(message)


class NotificationAvecChiffrement(DecorateurNotification):
    """[OK] Extension : Ajoute chiffrement"""
    
    def envoyer(self, message: str):
        message_chiffre = message[::-1]  # Simulation
        print(f"[SECURISE] Message chiffré")
        self.notification.envoyer(message_chiffre)


# Composition de décorateurs
print("=== NOTIFICATION AVEC EXTENSIONS ===\n")

notif = NotificationBase()
notif_avec_log = NotificationAvecLog(notif)
notif_complete = NotificationAvecRetry(
    NotificationAvecChiffrement(notif_avec_log)
)

notif_complete.envoyer("Message important")

"""
[OK] Chaque décorateur = extension
[OK] NotificationBase jamais modifiée
[OK] Combinaisons infinies possibles


[CLE] COMMENT RESPECTER OCP ?

1. IDENTIFIER POINTS DE VARIATION
   Qu'est-ce qui peut changer ?

2. CRÉER ABSTRACTIONS
   Interface ou classe abstraite

3. IMPLÉMENTER VARIATIONS
   Classes concrètes

4. UTILISER POLYMORPHISME
   Code client utilise abstraction
"""


# ----------------------------------------------------------------------------
# 3⃣ LISKOV SUBSTITUTION PRINCIPLE (LSP)
# ----------------------------------------------------------------------------

"""
═══════════════════════════════════════════════════════════════════════════
PRINCIPE 3 : LISKOV SUBSTITUTION PRINCIPLE (LSP)
═══════════════════════════════════════════════════════════════════════════

DÉFINITION :
"Les objets d'une classe dérivée doivent pouvoir remplacer 
 les objets de la classe de base sans altérer le fonctionnement du programme"

[IDEE] Reformulation simple :
Si S est sous-type de T, alors objets de type T peuvent être 
remplacés par objets de type S sans casser le programme


POURQUOI ?

[OK] Polymorphisme fiable
[OK] Comportement prévisible
[OK] Hiérarchies cohérentes


RÈGLES LSP :

1. SIGNATURE : Sous-classe garde signature méthodes
2. PRÉCONDITIONS : Sous-classe ne renforce pas préconditions
3. POSTCONDITIONS : Sous-classe respecte postconditions
4. INVARIANTS : Sous-classe préserve invariants
5. TYPES : Paramètres et retours compatibles


PROBLÈME : VIOLATION LSP
"""

# [X] EXEMPLE CLASSIQUE : Rectangle/Carré

class Rectangle:
    """Rectangle avec largeur et hauteur"""
    
    def __init__(self, largeur: float, hauteur: float):
        self._largeur = largeur
        self._hauteur = hauteur
    
    @property
    def largeur(self):
        return self._largeur
    
    @largeur.setter
    def largeur(self, valeur):
        self._largeur = valeur
    
    @property
    def hauteur(self):
        return self._hauteur
    
    @hauteur.setter
    def hauteur(self, valeur):
        self._hauteur = valeur
    
    def aire(self):
        return self._largeur * self._hauteur


class Carre(Rectangle):
    """
    [X] VIOLATION LSP
    
    Carré EST-UN Rectangle mathématiquement,
    mais PAS en programmation !
    """
    
    @property
    def largeur(self):
        return self._largeur
    
    @largeur.setter
    def largeur(self, valeur):
        # [X] Modifie aussi hauteur !
        self._largeur = valeur
        self._hauteur = valeur  # Couplage
    
    @property
    def hauteur(self):
        return self._hauteur
    
    @hauteur.setter
    def hauteur(self, valeur):
        # [X] Modifie aussi largeur !
        self._largeur = valeur
        self._hauteur = valeur


# Test qui prouve la violation
def test_rectangle(rect: Rectangle):
    """
    Fonction qui attend un Rectangle
    Devrait fonctionner avec Rectangle ET sous-classes
    """
    rect.largeur = 5
    rect.hauteur = 4
    
    # Assertion : aire devrait être 20
    assert rect.aire() == 20, f"Attendu 20, obtenu {rect.aire()}"
    print(f"[OK] Test OK: aire = {rect.aire()}")


print("\n=== VIOLATION LSP : Rectangle/Carré ===\n")

# Test avec Rectangle
print("Test avec Rectangle:")
r = Rectangle(0, 0)
test_rectangle(r)  # [OK] OK

# Test avec Carré
print("\nTest avec Carré:")
c = Carre(0, 0)
try:
    test_rectangle(c)  # [X] ÉCHEC !
except AssertionError as e:
    print(f"[X] Test échoue: {e}")
    # Aire = 16 (4x4) au lieu de 20 (5x4)

"""
Problème :
- Carré ne peut pas SUBSTITUER Rectangle
- Comportement inattendu
- Violation LSP


[OK] SOLUTION : NE PAS UTILISER HÉRITAGE
"""

from abc import ABC, abstractmethod

class Forme(ABC):
    """Interface commune"""
    
    @abstractmethod
    def aire(self) -> float:
        pass


class Rectangle(Forme):
    """Rectangle (indépendant)"""
    
    def __init__(self, largeur: float, hauteur: float):
        self.largeur = largeur
        self.hauteur = hauteur
    
    def aire(self) -> float:
        return self.largeur * self.hauteur


class Carre(Forme):
    """Carré (indépendant)"""
    
    def __init__(self, cote: float):
        self.cote = cote
    
    def aire(self) -> float:
        return self.cote ** 2


# Maintenant pas de problème de substitution
print("\n=== RESPECT LSP : Formes séparées ===\n")

formes = [
    Rectangle(5, 4),
    Carre(5)
]

for forme in formes:
    print(f"{forme.__class__.__name__}: aire = {forme.aire()}")


"""
EXEMPLE VIOLATION : Oiseau/Pingouin
"""

# [X] VIOLATION LSP

class Oiseau:
    """Oiseau qui peut voler"""
    
    def voler(self):
        print("Je vole")


class Pingouin(Oiseau):
    """
    [X] VIOLATION LSP
    
    Pingouin EST-UN Oiseau mais ne peut PAS voler
    """
    
    def voler(self):
        raise Exception("Je ne peux pas voler !")


def faire_voler_oiseau(oiseau: Oiseau):
    """Attend un oiseau qui vole"""
    oiseau.voler()


print("\n=== VIOLATION LSP : Oiseau/Pingouin ===\n")

aigle = Oiseau()
faire_voler_oiseau(aigle)  # [OK] OK

pingouin = Pingouin()
try:
    faire_voler_oiseau(pingouin)  # [X] Exception !
except Exception as e:
    print(f"[X] Erreur: {e}")


# [OK] SOLUTION : Séparer capacités

class Oiseau(ABC):
    """Oiseau de base"""
    
    @abstractmethod
    def se_deplacer(self):
        pass


class OiseauVolant(Oiseau):
    """Oiseau qui vole"""
    
    def se_deplacer(self):
        self.voler()
    
    def voler(self):
        print("Je vole")


class OiseauNageur(Oiseau):
    """Oiseau qui nage"""
    
    def se_deplacer(self):
        self.nager()
    
    def nager(self):
        print("Je nage")


class Aigle(OiseauVolant):
    """Aigle vole"""
    pass


class Pingouin(OiseauNageur):
    """Pingouin nage"""
    pass


print("\n=== RESPECT LSP : Capacités séparées ===\n")

oiseaux = [Aigle(), Pingouin()]

for oiseau in oiseaux:
    print(f"{oiseau.__class__.__name__}:", end=" ")
    oiseau.se_deplacer()


"""
EXEMPLE COMPLET : Compte bancaire
"""

# [X] VIOLATION LSP

class CompteBancaire:
    """Compte avec retrait"""
    
    def __init__(self, solde: float):
        self.solde = solde
    
    def retirer(self, montant: float):
        """Retirer argent"""
        if montant <= self.solde:
            self.solde -= montant
            return True
        return False


class CompteEpargne(CompteBancaire):
    """
    [X] VIOLATION LSP potentielle
    
    Ajoute restriction (précondition plus forte)
    """
    
    def __init__(self, solde: float, nb_retraits_max: int = 3):
        super().__init__(solde)
        self.nb_retraits = 0
        self.nb_retraits_max = nb_retraits_max
    
    def retirer(self, montant: float):
        """
        [X] Précondition renforcée
        
        Parent: juste solde suffisant
        Enfant: solde suffisant ET limite retraits
        """
        if self.nb_retraits >= self.nb_retraits_max:
            print("[X] Limite de retraits atteinte")
            return False
        
        if super().retirer(montant):
            self.nb_retraits += 1
            return True
        return False


def traiter_retraits(compte: CompteBancaire):
    """Fonction qui attend CompteBancaire"""
    for i in range(5):
        if compte.retirer(100):
            print(f"  Retrait {i+1} réussi")
        else:
            print(f"  Retrait {i+1} échoué")


print("\n=== VIOLATION LSP : Compte épargne ===\n")

print("Compte normal:")
compte = CompteBancaire(1000)
traiter_retraits(compte)  # 5 retraits OK

print("\nCompte épargne:")
epargne = CompteEpargne(1000, nb_retraits_max=3)
traiter_retraits(epargne)  # Seuls 3 retraits OK (comportement inattendu)


# [OK] SOLUTION : Interfaces séparées

class CompteSansRestriction(ABC):
    """Compte sans limite de retraits"""
    
    @abstractmethod
    def retirer(self, montant: float) -> bool:
        pass


class CompteAvecRestrictions(ABC):
    """Compte avec restrictions"""
    
    @abstractmethod
    def retirer(self, montant: float) -> bool:
        pass
    
    @abstractmethod
    def retraits_restants(self) -> int:
        pass


"""
[CLE] COMMENT RESPECTER LSP ?

1. TESTER SUBSTITUTION
   Remplacer parent par enfant -> doit fonctionner

2. NE PAS RENFORCER PRÉCONDITIONS
   Enfant accepte au moins ce que parent accepte

3. NE PAS AFFAIBLIR POSTCONDITIONS
   Enfant garantit au moins ce que parent garantit

4. PRÉSERVER INVARIANTS
   Règles du parent restent vraies

5. SI HÉRITAGE PROBLÉMATIQUE
   -> Utiliser composition ou interfaces séparées
"""


# Ce fichier continue...
# ============================================================================

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 4 (SUITE) : PRINCIPES SOLID (FIN)
# ============================================================================
#
# [OBJECTIF] CONTENU :
# - Fin Chapitre 18 : ISP et DIP
# - Exercices pratiques SOLID
# - Récapitulatif complet SOLID
#
# [TEMPS] TEMPS : ~3-4 heures
# [DOCS] PRÉREQUIS : SRP, OCP, LSP
# ============================================================================


# Suite du Chapitre 18 : Principes SOLID


# ----------------------------------------------------------------------------
# 4⃣ INTERFACE SEGREGATION PRINCIPLE (ISP)
# ----------------------------------------------------------------------------

"""
═══════════════════════════════════════════════════════════════════════════
PRINCIPE 4 : INTERFACE SEGREGATION PRINCIPLE (ISP)
═══════════════════════════════════════════════════════════════════════════

DÉFINITION :
"Aucun client ne devrait dépendre de méthodes qu'il n'utilise pas"

[IDEE] Reformulation :
Mieux vaut plusieurs petites interfaces spécifiques
qu'une grosse interface générale


POURQUOI ?

[OK] Évite dépendances inutiles
[OK] Interfaces ciblées et cohérentes
[OK] Facilite implémentation
[OK] Évite méthodes vides/exceptions


PROBLÈME : VIOLATION ISP
"""

# [X] GROSSE INTERFACE (Fat Interface)

from abc import ABC, abstractmethod

class Travailleur(ABC):
    """
    [X] VIOLATION ISP
    
    Interface trop large
    Tous les travailleurs ne font pas tout !
    """
    
    @abstractmethod
    def travailler(self):
        """Tous les travailleurs travaillent"""
        pass
    
    @abstractmethod
    def manger(self):
        """Tous les travailleurs mangent"""
        pass
    
    @abstractmethod
    def dormir(self):
        """Tous les travailleurs dorment"""
        pass
    
    @abstractmethod
    def se_recharger(self):
        """[X] Seulement pour robots !"""
        pass


class Humain(Travailleur):
    """Humain doit implémenter méthodes inutiles"""
    
    def travailler(self):
        print("[CHANTIER] Humain travaille")
    
    def manger(self):
        print("[FORK_AND_KNIFE_WITH_PLATE] Humain mange")
    
    def dormir(self):
        print("[SLEEPING_FACE] Humain dort")
    
    def se_recharger(self):
        """[X] Humain ne se recharge pas !"""
        raise NotImplementedError("Humain ne peut pas se recharger")


class Robot(Travailleur):
    """Robot doit implémenter méthodes inutiles"""
    
    def travailler(self):
        print("[BOT] Robot travaille")
    
    def manger(self):
        """[X] Robot ne mange pas !"""
        raise NotImplementedError("Robot ne mange pas")
    
    def dormir(self):
        """[X] Robot ne dort pas !"""
        raise NotImplementedError("Robot ne dort pas")
    
    def se_recharger(self):
        print("[PLUGIN] Robot se recharge")


# Test problématique
print("\n=== VIOLATION ISP ===\n")

def gerer_pause_dejeuner(travailleur: Travailleur):
    """Fonction qui suppose que tous peuvent manger"""
    travailleur.manger()


humain = Humain()
gerer_pause_dejeuner(humain)  # [OK] OK

robot = Robot()
try:
    gerer_pause_dejeuner(robot)  # [X] Exception !
except NotImplementedError as e:
    print(f"[X] Erreur: {e}")

"""
Problèmes :
- Méthodes inutiles obligatoires
- Exceptions à gérer
- Interfaces peu claires
- Couplage inutile
"""


# [OK] SOLUTION : PETITES INTERFACES SÉGRÉGÉES

class Travaillable(ABC):
    """Interface : Capacité de travailler"""
    
    @abstractmethod
    def travailler(self):
        pass


class Mangeable(ABC):
    """Interface : Capacité de manger"""
    
    @abstractmethod
    def manger(self):
        pass


class Dormable(ABC):
    """Interface : Capacité de dormir"""
    
    @abstractmethod
    def dormir(self):
        pass


class Rechargeable(ABC):
    """Interface : Capacité de se recharger"""
    
    @abstractmethod
    def se_recharger(self):
        pass


class Humain(Travaillable, Mangeable, Dormable):
    """
    [OK] RESPECTE ISP
    
    Implémente SEULEMENT interfaces pertinentes
    """
    
    def travailler(self):
        print("[CHANTIER] Humain travaille")
    
    def manger(self):
        print("[FORK_AND_KNIFE_WITH_PLATE] Humain mange")
    
    def dormir(self):
        print("[SLEEPING_FACE] Humain dort")


class Robot(Travaillable, Rechargeable):
    """
    [OK] RESPECTE ISP
    
    Implémente SEULEMENT interfaces pertinentes
    """
    
    def travailler(self):
        print("[BOT] Robot travaille")
    
    def se_recharger(self):
        print("[PLUGIN] Robot se recharge")


# Fonctions spécifiques
def gerer_pause_dejeuner(etre: Mangeable):
    """Accepte seulement ce qui peut manger"""
    etre.manger()


def gerer_recharge(appareil: Rechargeable):
    """Accepte seulement ce qui peut se recharger"""
    appareil.se_recharger()


print("\n=== RESPECT ISP ===\n")

humain = Humain()
gerer_pause_dejeuner(humain)  # [OK] OK

robot = Robot()
gerer_recharge(robot)  # [OK] OK

# Type checking empêche erreurs
# gerer_pause_dejeuner(robot)  # [X] Erreur de type (si type hints)

"""
[OK] Avantages :
- Interfaces ciblées
- Pas de méthodes inutiles
- Type safety
- Clarté du code
"""


"""
EXEMPLE COMPLET : IMPRIMANTE MULTIFONCTION
"""

# [X] VIOLATION ISP

class Imprimante(ABC):
    """
    [X] Interface trop large
    
    Toutes imprimantes ne font pas tout
    """
    
    @abstractmethod
    def imprimer(self, document):
        pass
    
    @abstractmethod
    def scanner(self, document):
        pass
    
    @abstractmethod
    def faxer(self, document):
        pass
    
    @abstractmethod
    def agrafer(self, document):
        pass


class ImprimanteSimple(Imprimante):
    """Imprimante simple : seulement imprimer"""
    
    def imprimer(self, document):
        print(f"[IMPRIMANTE] Impression : {document}")
    
    def scanner(self, document):
        """[X] Pas de scanner"""
        raise NotImplementedError("Pas de scanner")
    
    def faxer(self, document):
        """[X] Pas de fax"""
        raise NotImplementedError("Pas de fax")
    
    def agrafer(self, document):
        """[X] Pas d'agrafeuse"""
        raise NotImplementedError("Pas d'agrafeuse")


# [OK] SOLUTION : INTERFACES SÉGRÉGÉES

class Imprimable(ABC):
    """Interface : Impression"""
    
    @abstractmethod
    def imprimer(self, document):
        pass


class Scannable(ABC):
    """Interface : Scan"""
    
    @abstractmethod
    def scanner(self, document):
        pass


class Faxable(ABC):
    """Interface : Fax"""
    
    @abstractmethod
    def faxer(self, document):
        pass


class Agrafable(ABC):
    """Interface : Agrafage"""
    
    @abstractmethod
    def agrafer(self, document):
        pass


class ImprimanteSimple(Imprimable):
    """[OK] Seulement impression"""
    
    def imprimer(self, document):
        print(f"[IMPRIMANTE] Impression : {document}")


class ImprimanteScanner(Imprimable, Scannable):
    """[OK] Impression + Scan"""
    
    def imprimer(self, document):
        print(f"[IMPRIMANTE] Impression : {document}")
    
    def scanner(self, document):
        print(f"[CAMERA] Scan : {document}")


class ImprimanteMultifonction(Imprimable, Scannable, Faxable, Agrafable):
    """[OK] Toutes les fonctionnalités"""
    
    def imprimer(self, document):
        print(f"[IMPRIMANTE] Impression : {document}")
    
    def scanner(self, document):
        print(f"[CAMERA] Scan : {document}")
    
    def faxer(self, document):
        print(f"[FAX_MACHINE] Fax : {document}")
    
    def agrafer(self, document):
        print(f"[PAPERCLIP] Agrafage : {document}")


# Utilisation
print("\n=== IMPRIMANTES (ISP) ===\n")

def utiliser_imprimante(imprimante: Imprimable):
    """Fonction qui a besoin d'imprimer"""
    imprimante.imprimer("Document")


def utiliser_scanner(scanner: Scannable):
    """Fonction qui a besoin de scanner"""
    scanner.scanner("Document")


simple = ImprimanteSimple()
utiliser_imprimante(simple)  # [OK] OK
# utiliser_scanner(simple)  # [X] Type error

multifonction = ImprimanteMultifonction()
utiliser_imprimante(multifonction)  # [OK] OK
utiliser_scanner(multifonction)      # [OK] OK


"""
EXEMPLE : API REST
"""

# [X] VIOLATION ISP

class CRUDRepository(ABC):
    """[X] Tous les repositories ne font pas CRUD complet"""
    
    @abstractmethod
    def create(self, entity):
        pass
    
    @abstractmethod
    def read(self, id):
        pass
    
    @abstractmethod
    def update(self, id, entity):
        pass
    
    @abstractmethod
    def delete(self, id):
        pass


class ReadOnlyRepository(CRUDRepository):
    """Repository lecture seule (logs, analytics)"""
    
    def create(self, entity):
        raise NotImplementedError("Lecture seule")
    
    def read(self, id):
        return f"Entité {id}"
    
    def update(self, id, entity):
        raise NotImplementedError("Lecture seule")
    
    def delete(self, id):
        raise NotImplementedError("Lecture seule")


# [OK] SOLUTION

class Readable(ABC):
    """Interface : Lecture"""
    
    @abstractmethod
    def read(self, id):
        pass


class Writable(ABC):
    """Interface : Écriture"""
    
    @abstractmethod
    def create(self, entity):
        pass
    
    @abstractmethod
    def update(self, id, entity):
        pass


class Deletable(ABC):
    """Interface : Suppression"""
    
    @abstractmethod
    def delete(self, id):
        pass


class ReadOnlyRepository(Readable):
    """[OK] Seulement lecture"""
    
    def read(self, id):
        print(f"[GUIDE] Lecture entité {id}")
        return f"Entité {id}"


class FullRepository(Readable, Writable, Deletable):
    """[OK] CRUD complet"""
    
    def read(self, id):
        print(f"[GUIDE] Lecture entité {id}")
        return f"Entité {id}"
    
    def create(self, entity):
        print(f"+ Création : {entity}")
    
    def update(self, id, entity):
        print(f"[EDIT] Mise à jour {id} : {entity}")
    
    def delete(self, id):
        print(f"[SUPPRIMER] Suppression {id}")


"""
[CLE] COMMENT RESPECTER ISP ?

1. IDENTIFIER GROUPES DE MÉTHODES
   Quelles méthodes vont ensemble ?

2. CRÉER INTERFACES CIBLÉES
   Une interface par responsabilité

3. COMPOSITION D'INTERFACES
   Classes implémentent ce dont elles ont besoin

4. PAS DE MÉTHODES VIDES
   Si méthode vide -> mauvaise interface
"""


# ----------------------------------------------------------------------------
# 5⃣ DEPENDENCY INVERSION PRINCIPLE (DIP)
# ----------------------------------------------------------------------------

"""
═══════════════════════════════════════════════════════════════════════════
PRINCIPE 5 : DEPENDENCY INVERSION PRINCIPLE (DIP)
═══════════════════════════════════════════════════════════════════════════

DÉFINITION :
1. Modules de haut niveau ne doivent pas dépendre de modules de bas niveau
   Les deux doivent dépendre d'abstractions
2. Abstractions ne doivent pas dépendre de détails
   Détails doivent dépendre d'abstractions

[IDEE] Reformulation :
Dépendre d'interfaces/abstractions, pas d'implémentations concrètes


POURQUOI ?

[OK] Découplage
[OK] Testabilité (mocking)
[OK] Flexibilité
[OK] Inversion de contrôle


PROBLÈME : VIOLATION DIP
"""

# [X] DÉPENDANCE CONCRÈTE

class MySQLDatabase:
    """Implémentation concrète de BDD"""
    
    def connect(self):
        print("Connexion à MySQL")
    
    def query(self, sql):
        print(f"MySQL: {sql}")
        return ["résultat1", "résultat2"]


class UserService:
    """
    [X] VIOLATION DIP
    
    Service de haut niveau DÉPEND de MySQLDatabase (bas niveau)
    """
    
    def __init__(self):
        self.db = MySQLDatabase()  # [X] Dépendance concrète
    
    def get_user(self, user_id):
        self.db.connect()
        results = self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
        return results


"""
Problèmes :
- UserService couplé à MySQL
- Impossible de changer de BDD sans modifier UserService
- Impossible de tester sans MySQL
- Pas de flexibilité
"""


# [OK] SOLUTION : DÉPENDANCE ABSTRAITE

from abc import ABC, abstractmethod

class Database(ABC):
    """
    [OK] ABSTRACTION
    
    Interface indépendante de l'implémentation
    """
    
    @abstractmethod
    def connect(self):
        pass
    
    @abstractmethod
    def query(self, sql: str) -> list:
        pass


class MySQLDatabase(Database):
    """Implémentation MySQL"""
    
    def connect(self):
        print("Connexion à MySQL")
    
    def query(self, sql: str) -> list:
        print(f"MySQL: {sql}")
        return ["résultat1", "résultat2"]


class PostgreSQLDatabase(Database):
    """Implémentation PostgreSQL"""
    
    def connect(self):
        print("Connexion à PostgreSQL")
    
    def query(self, sql: str) -> list:
        print(f"PostgreSQL: {sql}")
        return ["résultat1", "résultat2"]


class MockDatabase(Database):
    """Implémentation pour tests"""
    
    def connect(self):
        print("Connexion mock")
    
    def query(self, sql: str) -> list:
        print(f"Mock: {sql}")
        return ["mock1", "mock2"]


class UserService:
    """
    [OK] RESPECTE DIP
    
    Dépend de Database (abstraction), pas d'implémentation
    """
    
    def __init__(self, database: Database):
        """
        [IDEE] INJECTION DE DÉPENDANCE
        
        Database fournie de l'extérieur
        """
        self.db = database
    
    def get_user(self, user_id):
        self.db.connect()
        results = self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
        return results


# Utilisation
print("\n=== DEPENDENCY INVERSION ===\n")

# En production : MySQL
mysql_db = MySQLDatabase()
service_prod = UserService(mysql_db)
service_prod.get_user(1)

print()

# En développement : PostgreSQL
postgres_db = PostgreSQLDatabase()
service_dev = UserService(postgres_db)
service_dev.get_user(1)

print()

# En test : Mock
mock_db = MockDatabase()
service_test = UserService(mock_db)
service_test.get_user(1)

"""
[OK] Avantages :
- UserService découplé de BDD spécifique
- Facile de changer de BDD
- Testable avec mock
- Flexible et extensible
"""


"""
EXEMPLE COMPLET : SYSTÈME DE NOTIFICATION
"""

# [X] VIOLATION DIP

class EmailSender:
    """Implémentation concrète"""
    
    def send_email(self, to: str, message: str):
        print(f"[EMAIL] Email à {to}: {message}")


class NotificationService:
    """[X] Dépend de EmailSender (concret)"""
    
    def __init__(self):
        self.email_sender = EmailSender()
    
    def notify(self, user: str, message: str):
        self.email_sender.send_email(user, message)


# [OK] SOLUTION : ABSTRACTION

class NotificationSender(ABC):
    """[OK] Abstraction"""
    
    @abstractmethod
    def send(self, recipient: str, message: str):
        pass


class EmailSender(NotificationSender):
    """Implémentation : Email"""
    
    def send(self, recipient: str, message: str):
        print(f"[EMAIL] Email à {recipient}: {message}")


class SMSSender(NotificationSender):
    """Implémentation : SMS"""
    
    def send(self, recipient: str, message: str):
        print(f"[MOBILE] SMS à {recipient}: {message}")


class PushSender(NotificationSender):
    """Implémentation : Push"""
    
    def send(self, recipient: str, message: str):
        print(f"[NOTIF] Push à {recipient}: {message}")


class SlackSender(NotificationSender):
    """Implémentation : Slack"""
    
    def send(self, recipient: str, message: str):
        print(f"[SPEECH_BALLOON] Slack à {recipient}: {message}")


class NotificationService:
    """
    [OK] Dépend de NotificationSender (abstraction)
    
    Peut utiliser N'IMPORTE QUELLE implémentation
    """
    
    def __init__(self, sender: NotificationSender):
        self.sender = sender
    
    def notify(self, user: str, message: str):
        self.sender.send(user, message)


# Utilisation flexible
print("\n=== NOTIFICATIONS (DIP) ===\n")

# Choisir canal selon contexte
notifications = [
    (EmailSender(), "user@example.com", "Email de bienvenue"),
    (SMSSender(), "+33612345678", "Code de vérification"),
    (PushSender(), "device_token", "Nouvelle notification"),
    (SlackSender(), "#general", "Déploiement réussi")
]

for sender, recipient, message in notifications:
    service = NotificationService(sender)
    service.notify(recipient, message)


"""
DEPENDENCY INJECTION : PATTERNS
"""

# Pattern 1 : Constructor Injection (le plus courant)
class ServiceA:
    def __init__(self, dependency: Database):
        """Injection par constructeur"""
        self.db = dependency


# Pattern 2 : Setter Injection
class ServiceB:
    def __init__(self):
        self.db = None
    
    def set_database(self, database: Database):
        """Injection par setter"""
        self.db = database


# Pattern 3 : Interface Injection
class DatabaseAware(ABC):
    @abstractmethod
    def set_database(self, database: Database):
        pass


class ServiceC(DatabaseAware):
    def __init__(self):
        self.db = None
    
    def set_database(self, database: Database):
        self.db = database


"""
INVERSION OF CONTROL (IoC) CONTAINER
"""

class Container:
    """
    Simple IoC Container
    
    Gère création et injection de dépendances
    """
    
    def __init__(self):
        self._services = {}
    
    def register(self, interface, implementation):
        """Enregistrer service"""
        self._services[interface] = implementation
    
    def resolve(self, interface):
        """Résoudre dépendance"""
        implementation = self._services.get(interface)
        if not implementation:
            raise ValueError(f"Service non enregistré : {interface}")
        
        # Créer instance
        # (version simplifiée, ne gère pas dépendances récursives)
        return implementation()


# Configuration du container
print("\n=== IOC CONTAINER ===\n")

container = Container()

# Enregistrer services
container.register(Database, MySQLDatabase)
container.register(NotificationSender, EmailSender)

# Résoudre dépendances
db = container.resolve(Database)
sender = container.resolve(NotificationSender)

# Utiliser
service = UserService(db)
service.get_user(1)

notif_service = NotificationService(sender)
notif_service.notify("user@example.com", "Test")


"""
EXEMPLE COMPLET : APPLICATION AVEC DIP
"""

# Abstractions
class Logger(ABC):
    @abstractmethod
    def log(self, message: str):
        pass


class Cache(ABC):
    @abstractmethod
    def get(self, key: str):
        pass
    
    @abstractmethod
    def set(self, key: str, value):
        pass


# Implémentations
class ConsoleLogger(Logger):
    def log(self, message: str):
        print(f"[LOG] {message}")


class FileLogger(Logger):
    def __init__(self, filename: str):
        self.filename = filename
    
    def log(self, message: str):
        print(f"[FILE:{self.filename}] {message}")


class MemoryCache(Cache):
    def __init__(self):
        self._cache = {}
    
    def get(self, key: str):
        return self._cache.get(key)
    
    def set(self, key: str, value):
        self._cache[key] = value


class RedisCache(Cache):
    def get(self, key: str):
        print(f"Redis GET {key}")
        return None
    
    def set(self, key: str, value):
        print(f"Redis SET {key} = {value}")


# Service de haut niveau
class ProductService:
    """
    [OK] RESPECTE DIP
    
    Dépend de Logger et Cache (abstractions)
    Pas d'implémentations concrètes
    """
    
    def __init__(self, database: Database, logger: Logger, cache: Cache):
        self.db = database
        self.logger = logger
        self.cache = cache
    
    def get_product(self, product_id: str):
        """Récupérer produit avec cache"""
        # Vérifier cache
        cached = self.cache.get(product_id)
        if cached:
            self.logger.log(f"Cache HIT pour produit {product_id}")
            return cached
        
        # Charger depuis BDD
        self.logger.log(f"Cache MISS pour produit {product_id}")
        self.db.connect()
        result = self.db.query(f"SELECT * FROM products WHERE id = '{product_id}'")
        
        # Mettre en cache
        self.cache.set(product_id, result)
        
        return result


# Configuration production
print("\n=== APPLICATION PRODUCTION ===\n")

prod_service = ProductService(
    database=PostgreSQLDatabase(),
    logger=FileLogger("app.log"),
    cache=RedisCache()
)

prod_service.get_product("PROD123")
prod_service.get_product("PROD123")  # Cache hit


# Configuration développement
print("\n=== APPLICATION DÉVELOPPEMENT ===\n")

dev_service = ProductService(
    database=MockDatabase(),
    logger=ConsoleLogger(),
    cache=MemoryCache()
)

dev_service.get_product("PROD456")
dev_service.get_product("PROD456")  # Cache hit


"""
[CLE] COMMENT RESPECTER DIP ?

1. IDENTIFIER DÉPENDANCES
   Quelles classes utilisent quelles autres ?

2. CRÉER ABSTRACTIONS
   Interface ou classe abstraite

3. DÉPENDRE D'ABSTRACTIONS
   Jamais de classes concrètes dans types

4. INJECTION DE DÉPENDANCE
   Fournir implémentations de l'extérieur

5. CONFIGURATION CENTRALISÉE
   IoC container ou composition root
"""


# ============================================================================
# [DOCS] RÉCAPITULATIF COMPLET SOLID
# ============================================================================

"""
═══════════════════════════════════════════════════════════════════════════
[BRAVO] FÉLICITATIONS ! PRINCIPES SOLID MAÎTRISÉS !
═══════════════════════════════════════════════════════════════════════════


┌────────────────────────────────────────────────────────────────────────┐
│ S - SINGLE RESPONSIBILITY PRINCIPLE                                    │
├────────────────────────────────────────────────────────────────────────┤
│ Une classe = UNE responsabilité                                        │
│ Une raison de changer                                                  │
│                                                                         │
│ [X] Classe Utilisateur avec validation, persistance, email, rapports    │
│ [OK] Classes séparées : Utilisateur, Validateur, Repository, etc.        │
└────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│ O - OPEN/CLOSED PRINCIPLE                                              │
├────────────────────────────────────────────────────────────────────────┤
│ Ouvert à extension, fermé à modification                               │
│                                                                         │
│ [X] if/else pour chaque nouveau type                                    │
│ [OK] Abstractions + Polymorphisme                                        │
│ [OK] Strategy, Decorator, etc.                                           │
└────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│ L - LISKOV SUBSTITUTION PRINCIPLE                                      │
├────────────────────────────────────────────────────────────────────────┤
│ Sous-classe doit pouvoir remplacer classe parent                       │
│                                                                         │
│ [X] Carré hérite Rectangle (comportement différent)                     │
│ [X] Pingouin hérite OiseauVolant (exception levée)                      │
│ [OK] Interfaces séparées selon capacités réelles                         │
└────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│ I - INTERFACE SEGREGATION PRINCIPLE                                    │
├────────────────────────────────────────────────────────────────────────┤
│ Plusieurs petites interfaces > Une grosse interface                    │
│ Client ne dépend que de ce qu'il utilise                              │
│                                                                         │
│ [X] Interface Travailleur avec manger, dormir, se_recharger            │
│ [OK] Interfaces séparées : Mangeable, Dormable, Rechargeable            │
└────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│ D - DEPENDENCY INVERSION PRINCIPLE                                     │
├────────────────────────────────────────────────────────────────────────┤
│ Dépendre d'abstractions, pas d'implémentations                         │
│ Injection de dépendances                                               │
│                                                                         │
│ [X] Service crée MySQLDatabase directement                              │
│ [OK] Service reçoit Database (abstraction) en paramètre                  │
└────────────────────────────────────────────────────────────────────────┘


═══════════════════════════════════════════════════════════════════════════
[OBJECTIF] BÉNÉFICES DE SOLID
═══════════════════════════════════════════════════════════════════════════

[OK] MAINTENABILITÉ
   Code facile à comprendre et modifier

[OK] TESTABILITÉ
   Classes découplées, mocking facile

[OK] FLEXIBILITÉ
   Changements localisés

[OK] RÉUTILISABILITÉ
   Composants indépendants

[OK] ÉVOLUTIVITÉ
   Ajout fonctionnalités sans régression


═══════════════════════════════════════════════════════════════════════════
[ATTENTION] QUAND NE PAS APPLIQUER SOLID ?
═══════════════════════════════════════════════════════════════════════════

[X] Sur-ingénierie
   Projet simple n'a pas besoin de toute cette complexité

[X] Premature optimization
   Appliquer SOLID quand besoin identifié

[X] Dogmatisme
   Pragmatisme > Purisme

[OK] Appliquer progressivement
   Refactorer quand code devient complexe


═══════════════════════════════════════════════════════════════════════════
[CLE] CHECKLIST SOLID
═══════════════════════════════════════════════════════════════════════════

AVANT D'ÉCRIRE UNE CLASSE :

[WHITE_SQUARE] SRP : Quelle est SA responsabilité unique ?
[WHITE_SQUARE] SRP : Combien de raisons de changer ?

AVANT D'AJOUTER UNE FONCTIONNALITÉ :

[WHITE_SQUARE] OCP : Puis-je l'ajouter sans modifier existant ?
[WHITE_SQUARE] OCP : Ai-je besoin d'abstraction ?

AVANT DE CRÉER HÉRITAGE :

[WHITE_SQUARE] LSP : Sous-classe peut-elle remplacer parent partout ?
[WHITE_SQUARE] LSP : Même contrat respecté ?

AVANT DE CRÉER INTERFACE :

[WHITE_SQUARE] ISP : Est-elle ciblée ou trop large ?
[WHITE_SQUARE] ISP : Tous les clients l'utilisent entièrement ?

AVANT D'UTILISER UNE CLASSE :

[WHITE_SQUARE] DIP : Puis-je dépendre d'abstraction ?
[WHITE_SQUARE] DIP : Injection de dépendance possible ?


═══════════════════════════════════════════════════════════════════════════
[DOCS] LIENS ENTRE SOLID ET DESIGN PATTERNS
═══════════════════════════════════════════════════════════════════════════

SRP <-> Facade, Proxy
    Séparer responsabilités

OCP <-> Strategy, Template Method, Decorator
    Extension sans modification

LSP <-> Factory Method, Abstract Factory
    Substitution via polymorphisme

ISP <-> Adapter
    Interfaces adaptées

DIP <-> Tous les patterns
    Dépendre d'abstractions


═══════════════════════════════════════════════════════════════════════════
[COURS] VOUS MAÎTRISEZ MAINTENANT
═══════════════════════════════════════════════════════════════════════════

[OK] 5 principes SOLID
[OK] Identifier violations
[OK] Refactorer code
[OK] Appliquer dans projets réels
[OK] Architecture logicielle solide


-> PROCHAINE ÉTAPE : BEST PRACTICES ET TESTING

Suite :
- Testing orienté objet
- Documentation professionnelle
- Performance et optimisation
- Anti-patterns à éviter


Excellent travail ! Architecture SOLID maîtrisée ! [CONSTRUCTION][RAPIDE]
"""

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 4 (SUITE) : TESTING ORIENTÉ OBJET
# ============================================================================
#
# [OBJECTIF] CONTENU :
# - Chapitre 19 : Testing Orienté Objet
#   * Introduction au testing
#   * unittest (framework standard)
#   * pytest (framework moderne)
#   * Mocking et fixtures
#   * Test Driven Development (TDD)
#   * Tests d'intégration
#   * Coverage et bonnes pratiques
#
# [TEMPS] TEMPS : ~6-7 heures
# [DOCS] PRÉREQUIS : Parties 1-4 (Design Patterns et SOLID)
# [IDEE] NIVEAU : Avancé - Pratiques professionnelles
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 19 : TESTING ORIENTÉ OBJET
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Pourquoi tester le code orienté objet
[OK] Écrire tests avec unittest
[OK] Utiliser pytest (framework moderne)
[OK] Mocker dépendances et comportements
[OK] Appliquer Test Driven Development (TDD)
[OK] Créer tests d'intégration
[OK] Mesurer couverture de code
[OK] Suivre bonnes pratiques de testing
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] INTRODUCTION AU TESTING
# ----------------------------------------------------------------------------

"""
POURQUOI TESTER ?

[OK] CONFIANCE
   Code fonctionne comme prévu

[OK] DOCUMENTATION
   Tests montrent comment utiliser le code

[OK] REFACTORING SÉCURISÉ
   Détecte régressions

[OK] DESIGN MEILLEUR
   Code testable = code bien conçu

[OK] MAINTENANCE
   Modifications sans peur de tout casser


TYPES DE TESTS

1. TESTS UNITAIRES (Unit Tests)
   -> Testent UNE unité (classe, méthode) isolément
   -> Rapides, nombreux
   -> Dépendances mockées

2. TESTS D'INTÉGRATION (Integration Tests)
   -> Testent plusieurs composants ensemble
   -> Plus lents, moins nombreux
   -> Dépendances réelles

3. TESTS FONCTIONNELS (Functional Tests)
   -> Testent fonctionnalités complètes
   -> Point de vue utilisateur
   -> End-to-end

4. TESTS DE RÉGRESSION (Regression Tests)
   -> Vérifient que bugs ne reviennent pas
   -> Spécifiques à un bug corrigé


PYRAMIDE DES TESTS

        /\
       /  \  E2E (peu, lents)
      /────\
     /      \ Intégration (moyens)
    /────────\
   /          \ Unitaires (nombreux, rapides)
  /────────────\


AAA PATTERN (Arrange-Act-Assert)

1. ARRANGE : Préparer données et contexte
2. ACT : Exécuter action à tester
3. ASSERT : Vérifier résultat


GIVEN-WHEN-THEN (variante AAA)

1. GIVEN : Étant donné un contexte
2. WHEN : Quand une action se produit
3. THEN : Alors résultat attendu
"""


# ----------------------------------------------------------------------------
# [TEST] UNITTEST : FRAMEWORK STANDARD
# ----------------------------------------------------------------------------

"""
UNITTEST = Framework de test intégré à Python

[IDEE] Inspiré de JUnit (Java)
Fait partie de la bibliothèque standard


STRUCTURE DE BASE
"""

import unittest

class TestCalculatrice(unittest.TestCase):
    """
    [IDEE] CLASSE DE TEST
    
    Hérite de unittest.TestCase
    Méthodes commençant par test_ sont exécutées
    """
    
    def test_addition(self):
        """Test addition simple"""
        # Arrange
        a = 5
        b = 3
        
        # Act
        resultat = a + b
        
        # Assert
        self.assertEqual(resultat, 8)
    
    def test_soustraction(self):
        """Test soustraction"""
        self.assertEqual(10 - 3, 7)
    
    def test_multiplication(self):
        """Test multiplication"""
        self.assertEqual(4 * 5, 20)


# Exécution (si script exécuté directement)
if __name__ == '__main__':
    unittest.main()


"""
MÉTHODES ASSERT COURANTES
"""

class TestAssertions(unittest.TestCase):
    """Exemples de méthodes assert"""
    
    def test_egalite(self):
        """Tester égalité"""
        self.assertEqual(1 + 1, 2)
        self.assertNotEqual(1 + 1, 3)
    
    def test_booleen(self):
        """Tester booléens"""
        self.assertTrue(True)
        self.assertFalse(False)
    
    def test_none(self):
        """Tester None"""
        valeur = None
        self.assertIsNone(valeur)
        
        valeur = "quelque chose"
        self.assertIsNotNone(valeur)
    
    def test_conteneurs(self):
        """Tester conteneurs"""
        liste = [1, 2, 3]
        self.assertIn(2, liste)
        self.assertNotIn(5, liste)
    
    def test_exceptions(self):
        """Tester qu'exception est levée"""
        with self.assertRaises(ValueError):
            int("pas_un_nombre")
    
    def test_almost_equal(self):
        """Tester égalité approximative (floats)"""
        self.assertAlmostEqual(0.1 + 0.2, 0.3)


"""
SETUP ET TEARDOWN : INITIALISATION ET NETTOYAGE
"""

class TestAvecSetup(unittest.TestCase):
    """Test avec initialisation et nettoyage"""
    
    def setUp(self):
        """
        [IDEE] setUp() : Appelé AVANT chaque test
        
        Prépare l'environnement
        """
        print("\n  -> setUp: Initialisation")
        self.liste = [1, 2, 3]
        self.compteur = 0
    
    def tearDown(self):
        """
        [IDEE] tearDown() : Appelé APRÈS chaque test
        
        Nettoie l'environnement
        """
        print("  -> tearDown: Nettoyage")
        self.liste = None
        self.compteur = None
    
    def test_un(self):
        """Premier test"""
        print("  -> test_un")
        self.assertEqual(len(self.liste), 3)
    
    def test_deux(self):
        """Deuxième test"""
        print("  -> test_deux")
        self.liste.append(4)
        self.assertEqual(len(self.liste), 4)
    
    def test_trois(self):
        """Troisième test"""
        print("  -> test_trois")
        # liste est [1, 2, 3] (pas [1, 2, 3, 4])
        # setUp est appelé avant CHAQUE test
        self.assertEqual(len(self.liste), 3)


"""
SETUP/TEARDOWN DE CLASSE : UNE FOIS POUR TOUTE LA CLASSE
"""

class TestAvecSetupClass(unittest.TestCase):
    """Setup/teardown au niveau classe"""
    
    @classmethod
    def setUpClass(cls):
        """
        [IDEE] setUpClass() : Appelé UNE FOIS au début
        
        Initialisation coûteuse (connexion BDD, etc.)
        """
        print("\n=== setUpClass: Connexion BDD ===")
        cls.connexion_db = "ConnexionDB"
    
    @classmethod
    def tearDownClass(cls):
        """
        [IDEE] tearDownClass() : Appelé UNE FOIS à la fin
        
        Nettoyage final
        """
        print("=== tearDownClass: Fermeture BDD ===")
        cls.connexion_db = None
    
    def test_a(self):
        print("  test_a")
        self.assertIsNotNone(self.connexion_db)
    
    def test_b(self):
        print("  test_b")
        self.assertIsNotNone(self.connexion_db)


"""
EXEMPLE COMPLET : TESTER UNE CLASSE
"""

# Classe à tester
class CompteBancaire:
    """Compte bancaire simple"""
    
    def __init__(self, numero, titulaire, solde_initial=0):
        if solde_initial < 0:
            raise ValueError("Solde initial ne peut pas être négatif")
        
        self.numero = numero
        self.titulaire = titulaire
        self.solde = solde_initial
    
    def deposer(self, montant):
        """Déposer argent"""
        if montant <= 0:
            raise ValueError("Montant doit être positif")
        
        self.solde += montant
        return self.solde
    
    def retirer(self, montant):
        """Retirer argent"""
        if montant <= 0:
            raise ValueError("Montant doit être positif")
        
        if montant > self.solde:
            raise ValueError("Solde insuffisant")
        
        self.solde -= montant
        return self.solde
    
    def transferer(self, montant, compte_destinataire):
        """Transférer vers autre compte"""
        self.retirer(montant)
        compte_destinataire.deposer(montant)


# Tests
class TestCompteBancaire(unittest.TestCase):
    """Tests pour CompteBancaire"""
    
    def setUp(self):
        """Créer compte pour chaque test"""
        self.compte = CompteBancaire("123", "Alice", 1000)
    
    def test_creation_compte(self):
        """Test création compte valide"""
        compte = CompteBancaire("456", "Bob", 500)
        
        self.assertEqual(compte.numero, "456")
        self.assertEqual(compte.titulaire, "Bob")
        self.assertEqual(compte.solde, 500)
    
    def test_creation_solde_negatif(self):
        """Test création avec solde négatif (doit échouer)"""
        with self.assertRaises(ValueError):
            CompteBancaire("789", "Charlie", -100)
    
    def test_depot(self):
        """Test dépôt valide"""
        nouveau_solde = self.compte.deposer(500)
        
        self.assertEqual(self.compte.solde, 1500)
        self.assertEqual(nouveau_solde, 1500)
    
    def test_depot_montant_negatif(self):
        """Test dépôt montant négatif (doit échouer)"""
        with self.assertRaises(ValueError):
            self.compte.deposer(-50)
    
    def test_depot_montant_zero(self):
        """Test dépôt zéro (doit échouer)"""
        with self.assertRaises(ValueError):
            self.compte.deposer(0)
    
    def test_retrait(self):
        """Test retrait valide"""
        nouveau_solde = self.compte.retirer(300)
        
        self.assertEqual(self.compte.solde, 700)
        self.assertEqual(nouveau_solde, 700)
    
    def test_retrait_solde_insuffisant(self):
        """Test retrait avec solde insuffisant"""
        with self.assertRaises(ValueError):
            self.compte.retirer(2000)
    
    def test_transfert(self):
        """Test transfert entre comptes"""
        compte_bob = CompteBancaire("456", "Bob", 500)
        
        self.compte.transferer(200, compte_bob)
        
        self.assertEqual(self.compte.solde, 800)
        self.assertEqual(compte_bob.solde, 700)


"""
ORGANISER TESTS EN SUITES
"""

def suite():
    """Créer suite de tests personnalisée"""
    suite = unittest.TestSuite()
    
    # Ajouter tests spécifiques
    suite.addTest(TestCompteBancaire('test_creation_compte'))
    suite.addTest(TestCompteBancaire('test_depot'))
    suite.addTest(TestCompteBancaire('test_retrait'))
    
    return suite


# Exécuter suite
if __name__ == '__main__':
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite())


"""
SKIP ET EXPECTED FAILURES
"""

class TestAvecSkip(unittest.TestCase):
    """Tests avec skip et expected failure"""
    
    @unittest.skip("Fonctionnalité pas encore implémentée")
    def test_fonction_future(self):
        """Test à ignorer temporairement"""
        pass
    
    @unittest.skipIf(True, "Condition non remplie")
    def test_conditionnel(self):
        """Test ignoré si condition vraie"""
        pass
    
    @unittest.skipUnless(False, "Nécessite environnement spécial")
    def test_environnement_special(self):
        """Test ignoré sauf si condition vraie"""
        pass
    
    @unittest.expectedFailure
    def test_bug_connu(self):
        """Test qui doit échouer (bug connu)"""
        self.assertEqual(1 + 1, 3)  # Bug connu


# ----------------------------------------------------------------------------
# [ANALYSE] PYTEST : FRAMEWORK MODERNE
# ----------------------------------------------------------------------------

"""
PYTEST = Framework de test moderne et puissant

[IDEE] Avantages sur unittest :
- Syntaxe plus simple (assert standard)
- Fixtures puissantes
- Paramétrage facile
- Plugins riches
- Découverte automatique


INSTALLATION

pip install pytest


STRUCTURE DE BASE (pytest)
"""

# test_calculatrice.py
# Pas besoin d'hériter de TestCase !

def test_addition():
    """Test simple avec pytest"""
    assert 2 + 2 == 4


def test_soustraction():
    """Autre test"""
    assert 5 - 3 == 2


def test_multiplication():
    """Assert standard Python"""
    resultat = 3 * 4
    assert resultat == 12


"""
EXÉCUTION PYTEST

Dans terminal :
pytest                      # Exécute tous les tests
pytest test_fichier.py      # Exécute tests d'un fichier
pytest -v                   # Verbose
pytest -k "test_addition"   # Tests matchant pattern


FIXTURES PYTEST
"""

import pytest

@pytest.fixture
def compte():
    """
    [IDEE] FIXTURE
    
    Prépare données pour tests
    Exécutée avant chaque test qui l'utilise
    """
    return CompteBancaire("123", "Alice", 1000)


def test_depot_avec_fixture(compte):
    """Test utilisant fixture"""
    compte.deposer(500)
    assert compte.solde == 1500


def test_retrait_avec_fixture(compte):
    """Autre test avec même fixture"""
    compte.retirer(300)
    assert compte.solde == 700


"""
FIXTURES AVEC SETUP/TEARDOWN
"""

@pytest.fixture
def fichier_temp():
    """
    Fixture avec setup et teardown
    
    yield sépare setup et teardown
    """
    # Setup : créer fichier
    import tempfile
    f = tempfile.NamedTemporaryFile(mode='w', delete=False)
    f.write("Test data")
    f.close()
    
    # Fournir fichier au test
    yield f.name
    
    # Teardown : nettoyer
    import os
    os.unlink(f.name)


def test_lecture_fichier(fichier_temp):
    """Test utilisant fichier temporaire"""
    with open(fichier_temp, 'r') as f:
        contenu = f.read()
    
    assert contenu == "Test data"


"""
SCOPES DE FIXTURES
"""

@pytest.fixture(scope="function")  # Défaut : avant chaque test
def fixture_function():
    print("\n-> Setup function")
    yield "function"
    print("-> Teardown function")


@pytest.fixture(scope="class")  # Une fois par classe
def fixture_class():
    print("\n-> Setup class")
    yield "class"
    print("-> Teardown class")


@pytest.fixture(scope="module")  # Une fois par module
def fixture_module():
    print("\n-> Setup module")
    yield "module"
    print("-> Teardown module")


@pytest.fixture(scope="session")  # Une fois pour toute la session
def fixture_session():
    print("\n-> Setup session")
    yield "session"
    print("-> Teardown session")


"""
PARAMÉTRAGE DE TESTS
"""

@pytest.mark.parametrize("a,b,attendu", [
    (2, 3, 5),
    (10, 5, 15),
    (100, 200, 300),
    (-1, 1, 0)
])
def test_addition_parametree(a, b, attendu):
    """
    [IDEE] PARAMÉTRAGE
    
    Un test exécuté avec plusieurs jeux de données
    """
    assert a + b == attendu


@pytest.mark.parametrize("montant,solde_final", [
    (100, 1100),
    (500, 1500),
    (1000, 2000)
])
def test_depot_parametree(compte, montant, solde_final):
    """Paramétrage avec fixture"""
    compte.deposer(montant)
    assert compte.solde == solde_final


"""
MARKERS PYTEST
"""

@pytest.mark.slow
def test_operation_lente():
    """Test marqué comme lent"""
    import time
    time.sleep(0.1)
    assert True


@pytest.mark.integration
def test_integration():
    """Test d'intégration"""
    assert True


@pytest.mark.skip(reason="Pas encore implémenté")
def test_futur():
    """Test à ignorer"""
    pass


@pytest.mark.xfail(reason="Bug connu")
def test_bug_connu():
    """Test qui doit échouer"""
    assert 1 + 1 == 3


# Exécution sélective :
# pytest -m slow        # Seulement tests lents
# pytest -m integration # Seulement intégration
# pytest -m "not slow"  # Tous sauf lents


"""
EXEMPLE COMPLET : TESTS PYTEST POUR CLASSE
"""

# conftest.py (fixtures partagées)
# Ce fichier est automatiquement chargé par pytest

@pytest.fixture
def compte_alice():
    """Compte d'Alice"""
    return CompteBancaire("123", "Alice", 1000)


@pytest.fixture
def compte_bob():
    """Compte de Bob"""
    return CompteBancaire("456", "Bob", 500)


# test_compte_bancaire.py

class TestCompteBancaire:
    """Groupe de tests pour CompteBancaire"""
    
    def test_creation(self):
        """Test création"""
        compte = CompteBancaire("789", "Charlie", 2000)
        
        assert compte.numero == "789"
        assert compte.titulaire == "Charlie"
        assert compte.solde == 2000
    
    def test_creation_solde_negatif(self):
        """Test création invalide"""
        with pytest.raises(ValueError, match="négatif"):
            CompteBancaire("999", "David", -100)
    
    def test_depot(self, compte_alice):
        """Test dépôt"""
        compte_alice.deposer(500)
        assert compte_alice.solde == 1500
    
    @pytest.mark.parametrize("montant", [0, -50, -100])
    def test_depot_invalide(self, compte_alice, montant):
        """Test dépôts invalides"""
        with pytest.raises(ValueError):
            compte_alice.deposer(montant)
    
    def test_retrait(self, compte_alice):
        """Test retrait"""
        compte_alice.retirer(300)
        assert compte_alice.solde == 700
    
    def test_retrait_solde_insuffisant(self, compte_alice):
        """Test retrait impossible"""
        with pytest.raises(ValueError, match="insuffisant"):
            compte_alice.retirer(2000)
    
    def test_transfert(self, compte_alice, compte_bob):
        """Test transfert entre comptes"""
        compte_alice.transferer(200, compte_bob)
        
        assert compte_alice.solde == 800
        assert compte_bob.solde == 700


# ----------------------------------------------------------------------------
# [SCENARIO] MOCKING : SIMULER DÉPENDANCES
# ----------------------------------------------------------------------------

"""
MOCKING = Simuler comportement d'objets

[IDEE] Pourquoi mocker ?
- Isoler code testé
- Éviter dépendances externes (BDD, API, fichiers)
- Contrôler comportements
- Accélérer tests


UNITTEST.MOCK (intégré Python 3.3+)
"""

from unittest.mock import Mock, MagicMock, patch, call

"""
MOCK BASIQUE
"""

def test_mock_simple():
    """Créer mock simple"""
    # Créer mock
    mock = Mock()
    
    # Configurer valeur retournée
    mock.return_value = 42
    
    # Appeler mock
    resultat = mock()
    
    # Vérifications
    assert resultat == 42
    mock.assert_called_once()


"""
MOCK AVEC MÉTHODES
"""

def test_mock_methodes():
    """Mock avec méthodes"""
    mock_db = Mock()
    
    # Configurer méthode
    mock_db.get_user.return_value = {"id": 1, "nom": "Alice"}
    
    # Utiliser
    user = mock_db.get_user(1)
    
    # Vérifications
    assert user["nom"] == "Alice"
    mock_db.get_user.assert_called_once_with(1)


"""
EXEMPLE : MOCKER DÉPENDANCES
"""

# Code à tester
class EmailService:
    """Service email"""
    
    def send_email(self, to, subject, body):
        """Envoyer email (vraie implémentation)"""
        # Code qui envoie vraiment email
        print(f"Envoi email à {to}")
        return True


class UserService:
    """Service utilisateur avec dépendance EmailService"""
    
    def __init__(self, email_service):
        self.email_service = email_service
    
    def create_user(self, nom, email):
        """Créer utilisateur et envoyer email bienvenue"""
        user = {"nom": nom, "email": email}
        
        # Envoyer email
        self.email_service.send_email(
            to=email,
            subject="Bienvenue",
            body=f"Bonjour {nom}"
        )
        
        return user


# Test avec mock
def test_create_user_envoie_email():
    """
    Test que create_user envoie email
    
    On ne veut PAS envoyer vrai email dans test !
    """
    # Créer mock EmailService
    mock_email = Mock()
    
    # Créer UserService avec mock
    service = UserService(mock_email)
    
    # Créer utilisateur
    user = service.create_user("Alice", "alice@example.com")
    
    # Vérifier email envoyé
    mock_email.send_email.assert_called_once_with(
        to="alice@example.com",
        subject="Bienvenue",
        body="Bonjour Alice"
    )
    
    # Vérifier utilisateur créé
    assert user["nom"] == "Alice"


"""
PATCH : REMPLACER TEMPORAIREMENT
"""

# Code à tester
import requests

class APIClient:
    """Client API"""
    
    def get_data(self, url):
        """Récupérer données depuis API"""
        response = requests.get(url)
        return response.json()


# Test avec patch
@patch('requests.get')
def test_api_client(mock_get):
    """
    [IDEE] PATCH
    
    Remplace requests.get par mock temporairement
    """
    # Configurer mock
    mock_response = Mock()
    mock_response.json.return_value = {"data": "test"}
    mock_get.return_value = mock_response
    
    # Tester
    client = APIClient()
    result = client.get_data("https://api.example.com/data")
    
    # Vérifications
    assert result == {"data": "test"}
    mock_get.assert_called_once_with("https://api.example.com/data")


"""
PATCH COMME CONTEXT MANAGER
"""

def test_api_avec_context_manager():
    """Patch avec with"""
    with patch('requests.get') as mock_get:
        # Configurer
        mock_get.return_value.json.return_value = {"status": "ok"}
        
        # Tester
        client = APIClient()
        result = client.get_data("https://api.example.com")
        
        assert result["status"] == "ok"


"""
PATCH COMME DÉCORATEUR DE CLASSE
"""

@patch('requests.get')
class TestAPIClient:
    """Patch appliqué à tous les tests de la classe"""
    
    def test_get_data(self, mock_get):
        """Test avec mock"""
        mock_get.return_value.json.return_value = {"id": 1}
        
        client = APIClient()
        result = client.get_data("https://api.example.com")
        
        assert result["id"] == 1
    
    def test_autre(self, mock_get):
        """Autre test avec même mock"""
        mock_get.return_value.json.return_value = {"id": 2}
        
        client = APIClient()
        result = client.get_data("https://api.example.com")
        
        assert result["id"] == 2


"""
MOCK AVEC EFFETS SECONDAIRES
"""

def test_mock_side_effect():
    """Mock avec effet secondaire"""
    mock = Mock()
    
    # Lever exception
    mock.side_effect = ValueError("Erreur test")
    
    with pytest.raises(ValueError, match="Erreur test"):
        mock()


def test_mock_valeurs_multiples():
    """Mock retournant valeurs différentes"""
    mock = Mock()
    
    # Retourner valeurs successives
    mock.side_effect = [1, 2, 3]
    
    assert mock() == 1
    assert mock() == 2
    assert mock() == 3


"""
EXEMPLE COMPLET : TESTER AVEC MOCKS
"""

# Code à tester
class Database:
    """Base de données"""
    
    def get_user(self, user_id):
        """Récupérer utilisateur"""
        # Vraie requête BDD
        pass
    
    def save_user(self, user):
        """Sauvegarder utilisateur"""
        # Vraie sauvegarde
        pass


class UserRepository:
    """Repository avec dépendance Database"""
    
    def __init__(self, database):
        self.database = database
    
    def find_user(self, user_id):
        """Trouver utilisateur"""
        return self.database.get_user(user_id)
    
    def update_email(self, user_id, new_email):
        """Mettre à jour email"""
        user = self.database.get_user(user_id)
        user['email'] = new_email
        self.database.save_user(user)
        return user


# Tests
class TestUserRepository:
    """Tests avec mocks"""
    
    def test_find_user(self):
        """Test find_user avec mock database"""
        # Créer mock
        mock_db = Mock()
        mock_db.get_user.return_value = {
            'id': 1,
            'nom': 'Alice',
            'email': 'alice@example.com'
        }
        
        # Créer repository avec mock
        repo = UserRepository(mock_db)
        
        # Tester
        user = repo.find_user(1)
        
        # Vérifier
        assert user['nom'] == 'Alice'
        mock_db.get_user.assert_called_once_with(1)
    
    def test_update_email(self):
        """Test update_email avec mock"""
        # Mock database
        mock_db = Mock()
        mock_db.get_user.return_value = {
            'id': 1,
            'nom': 'Bob',
            'email': 'old@example.com'
        }
        
        # Repository
        repo = UserRepository(mock_db)
        
        # Mettre à jour email
        user = repo.update_email(1, 'new@example.com')
        
        # Vérifier email changé
        assert user['email'] == 'new@example.com'
        
        # Vérifier appels
        mock_db.get_user.assert_called_once_with(1)
        mock_db.save_user.assert_called_once()
        
        # Vérifier argument de save_user
        saved_user = mock_db.save_user.call_args[0][0]
        assert saved_user['email'] == 'new@example.com'


"""
SPY : VÉRIFIER APPELS SANS CHANGER COMPORTEMENT
"""

def test_spy():
    """Spy enregistre appels mais garde comportement"""
    # Vraie fonction
    def vraie_fonction(x):
        return x * 2
    
    # Créer spy
    from unittest.mock import MagicMock
    spy = MagicMock(side_effect=vraie_fonction)
    
    # Utiliser
    resultat = spy(5)
    
    # Comportement préservé
    assert resultat == 10
    
    # Mais appels enregistrés
    spy.assert_called_once_with(5)


# Ce fichier continue...
# ============================================================================

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 4 (FIN) : TDD ET BEST PRACTICES DE TESTING
# ============================================================================
#
# [OBJECTIF] CONTENU :
# - Fin Chapitre 19 : TDD, Coverage, Best Practices
#   * Test Driven Development (TDD)
#   * Tests d'intégration
#   * Code Coverage
#   * Best practices de testing
#   * Exercices pratiques
#
# [TEMPS] TEMPS : ~3-4 heures
# [DOCS] PRÉREQUIS : Testing avec unittest/pytest et mocking
# ============================================================================


# Suite du Chapitre 19...


# ----------------------------------------------------------------------------
# [ROUGE] TEST DRIVEN DEVELOPMENT (TDD)
# ----------------------------------------------------------------------------

"""
TDD = Test Driven Development

[IDEE] Principe : Écrire tests AVANT le code

CYCLE TDD (Red-Green-Refactor)

[ROUGE] RED : Écrire test qui échoue
[VERT] GREEN : Écrire code minimum pour passer test
[BLEU] REFACTOR : Améliorer code sans casser tests


AVANTAGES TDD

[OK] Design meilleur (code testable dès le début)
[OK] Couverture 100% (tests d'abord)
[OK] Documentation vivante (tests = spec)
[OK] Moins de bugs
[OK] Refactoring confiant


EXEMPLE COMPLET TDD : CLASSE PANIER D'ACHAT
"""

import pytest

"""
ÉTAPE 1 : [ROUGE] RED - ÉCRIRE TEST QUI ÉCHOUE
"""

def test_panier_vide_au_depart():
    """Test : panier vide à la création"""
    panier = ShoppingCart()  # [X] Classe n'existe pas encore !
    
    assert panier.is_empty() is True
    assert panier.item_count == 0


"""
ÉTAPE 2 : [VERT] GREEN - CODE MINIMUM
"""

class ShoppingCart:
    """Panier d'achat - implémentation minimale"""
    
    def __init__(self):
        self.items = []
    
    def is_empty(self):
        return len(self.items) == 0
    
    @property
    def item_count(self):
        return len(self.items)


# [OK] Test passe maintenant !

"""
ÉTAPE 3 : NOUVEAU TEST [ROUGE]
"""

def test_ajouter_item_au_panier():
    """Test : ajouter item"""
    panier = ShoppingCart()
    
    panier.add_item("Laptop", 999, 1)
    
    assert panier.is_empty() is False
    assert panier.item_count == 1


"""
ÉTAPE 4 : [VERT] IMPLÉMENTER
"""

class ShoppingCart:
    """Panier avec ajout d'items"""
    
    def __init__(self):
        self.items = []
    
    def add_item(self, name, price, quantity):
        """Ajouter item"""
        self.items.append({
            'name': name,
            'price': price,
            'quantity': quantity
        })
    
    def is_empty(self):
        return len(self.items) == 0
    
    @property
    def item_count(self):
        return len(self.items)


"""
ÉTAPE 5 : NOUVEAU TEST [ROUGE]
"""

def test_calculer_total():
    """Test : calcul du total"""
    panier = ShoppingCart()
    
    panier.add_item("Laptop", 999, 1)
    panier.add_item("Mouse", 29, 2)
    
    assert panier.total() == 1057  # 999 + (29*2)


"""
ÉTAPE 6 : [VERT] IMPLÉMENTER
"""

class ShoppingCart:
    """Panier avec calcul total"""
    
    def __init__(self):
        self.items = []
    
    def add_item(self, name, price, quantity):
        self.items.append({
            'name': name,
            'price': price,
            'quantity': quantity
        })
    
    def is_empty(self):
        return len(self.items) == 0
    
    @property
    def item_count(self):
        return len(self.items)
    
    def total(self):
        """Calculer total"""
        return sum(
            item['price'] * item['quantity']
            for item in self.items
        )


"""
ÉTAPE 7 : [BLEU] REFACTOR - AMÉLIORER SANS CASSER TESTS
"""

class Item:
    """Item séparé (meilleur design)"""
    
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity
    
    @property
    def subtotal(self):
        return self.price * self.quantity


class ShoppingCart:
    """
    [BLEU] REFACTOR : Utiliser classe Item
    
    Tests passent toujours ! (API publique inchangée)
    """
    
    def __init__(self):
        self.items = []
    
    def add_item(self, name, price, quantity):
        """API inchangée"""
        item = Item(name, price, quantity)
        self.items.append(item)
    
    def is_empty(self):
        return len(self.items) == 0
    
    @property
    def item_count(self):
        return len(self.items)
    
    def total(self):
        return sum(item.subtotal for item in self.items)


"""
CONTINUER TDD : PLUS DE FONCTIONNALITÉS
"""

def test_retirer_item():
    """[ROUGE] Test : retirer item"""
    panier = ShoppingCart()
    
    panier.add_item("Laptop", 999, 1)
    panier.add_item("Mouse", 29, 1)
    
    panier.remove_item("Mouse")
    
    assert panier.item_count == 1
    assert panier.total() == 999


# [VERT] Implémenter
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, name, price, quantity):
        item = Item(name, price, quantity)
        self.items.append(item)
    
    def remove_item(self, name):
        """[VERT] Nouvelle fonctionnalité"""
        self.items = [
            item for item in self.items
            if item.name != name
        ]
    
    def is_empty(self):
        return len(self.items) == 0
    
    @property
    def item_count(self):
        return len(self.items)
    
    def total(self):
        return sum(item.subtotal for item in self.items)


def test_appliquer_code_promo():
    """[ROUGE] Test : code promo"""
    panier = ShoppingCart()
    panier.add_item("Laptop", 1000, 1)
    
    panier.apply_discount_code("PROMO10")
    
    assert panier.total() == 900  # -10%


# [VERT] Implémenter
class ShoppingCart:
    def __init__(self):
        self.items = []
        self.discount_rate = 0
    
    def add_item(self, name, price, quantity):
        item = Item(name, price, quantity)
        self.items.append(item)
    
    def remove_item(self, name):
        self.items = [item for item in self.items if item.name != name]
    
    def apply_discount_code(self, code):
        """[VERT] Appliquer code promo"""
        discount_codes = {
            "PROMO10": 0.10,
            "PROMO20": 0.20,
            "PROMO50": 0.50
        }
        self.discount_rate = discount_codes.get(code, 0)
    
    def is_empty(self):
        return len(self.items) == 0
    
    @property
    def item_count(self):
        return len(self.items)
    
    def total(self):
        subtotal = sum(item.subtotal for item in self.items)
        return subtotal * (1 - self.discount_rate)


"""
[OK] CLASSE COMPLÈTE DÉVELOPPÉE EN TDD

Toutes les fonctionnalités testées
Code propre et maintenable
"""


# ----------------------------------------------------------------------------
# [LIEN] TESTS D'INTÉGRATION
# ----------------------------------------------------------------------------

"""
TESTS D'INTÉGRATION = Tester plusieurs composants ensemble

Différence avec tests unitaires :
- Unitaires : un composant isolé (mocks)
- Intégration : plusieurs composants réels


EXEMPLE : SYSTÈME DE COMMANDE COMPLET
"""

class ProductRepository:
    """Repository produits"""
    
    def __init__(self):
        self.products = {
            'LAPTOP': {'name': 'Laptop', 'price': 999, 'stock': 5},
            'MOUSE': {'name': 'Mouse', 'price': 29, 'stock': 20},
            'KEYBOARD': {'name': 'Keyboard', 'price': 79, 'stock': 10}
        }
    
    def get_product(self, product_id):
        """Récupérer produit"""
        return self.products.get(product_id)
    
    def update_stock(self, product_id, quantity):
        """Mettre à jour stock"""
        if product_id in self.products:
            self.products[product_id]['stock'] -= quantity


class Order:
    """Commande"""
    
    def __init__(self, order_id):
        self.order_id = order_id
        self.items = []
        self.status = 'pending'
    
    def add_item(self, product, quantity):
        """Ajouter item"""
        self.items.append({
            'product': product,
            'quantity': quantity
        })
    
    def calculate_total(self):
        """Calculer total"""
        return sum(
            item['product']['price'] * item['quantity']
            for item in self.items
        )
    
    def complete(self):
        """Compléter commande"""
        self.status = 'completed'


class OrderService:
    """Service de commande (orchestration)"""
    
    def __init__(self, product_repo):
        self.product_repo = product_repo
    
    def create_order(self, order_id, items):
        """
        Créer commande complète
        
        items = [('LAPTOP', 1), ('MOUSE', 2)]
        """
        order = Order(order_id)
        
        # Vérifier stock et ajouter items
        for product_id, quantity in items:
            product = self.product_repo.get_product(product_id)
            
            if not product:
                raise ValueError(f"Produit {product_id} introuvable")
            
            if product['stock'] < quantity:
                raise ValueError(f"Stock insuffisant pour {product_id}")
            
            order.add_item(product, quantity)
            self.product_repo.update_stock(product_id, quantity)
        
        order.complete()
        return order


"""
TEST D'INTÉGRATION : TOUT LE SYSTÈME
"""

class TestOrderIntegration:
    """Tests d'intégration du système de commande"""
    
    @pytest.fixture
    def product_repo(self):
        """Repository réel (pas de mock)"""
        return ProductRepository()
    
    @pytest.fixture
    def order_service(self, product_repo):
        """Service avec repository réel"""
        return OrderService(product_repo)
    
    def test_create_order_success(self, order_service, product_repo):
        """
        [IDEE] TEST D'INTÉGRATION
        
        Teste interaction RÉELLE entre tous les composants
        """
        # Stock initial
        initial_laptop_stock = product_repo.get_product('LAPTOP')['stock']
        initial_mouse_stock = product_repo.get_product('MOUSE')['stock']
        
        # Créer commande
        order = order_service.create_order('ORDER-001', [
            ('LAPTOP', 1),
            ('MOUSE', 2)
        ])
        
        # Vérifications
        assert order.status == 'completed'
        assert len(order.items) == 2
        assert order.calculate_total() == 1057  # 999 + (29*2)
        
        # Vérifier stock mis à jour
        assert product_repo.get_product('LAPTOP')['stock'] == initial_laptop_stock - 1
        assert product_repo.get_product('MOUSE')['stock'] == initial_mouse_stock - 2
    
    def test_create_order_produit_inexistant(self, order_service):
        """Test commande avec produit inexistant"""
        with pytest.raises(ValueError, match="introuvable"):
            order_service.create_order('ORDER-002', [
                ('INEXISTANT', 1)
            ])
    
    def test_create_order_stock_insuffisant(self, order_service, product_repo):
        """Test commande avec stock insuffisant"""
        # Laptop a 5 en stock
        with pytest.raises(ValueError, match="Stock insuffisant"):
            order_service.create_order('ORDER-003', [
                ('LAPTOP', 10)  # Trop !
            ])
        
        # Stock pas modifié (transaction échouée)
        assert product_repo.get_product('LAPTOP')['stock'] == 5


"""
TESTS D'INTÉGRATION AVEC BASE DE DONNÉES

Pour DB réelle, utiliser :
- Base de test séparée
- Transactions avec rollback
- Fixtures pour données
"""

@pytest.fixture(scope="function")
def db_with_test_data():
    """
    Fixture : DB avec données de test
    
    Setup : Créer tables et données
    Teardown : Nettoyer
    """
    # Setup
    db = create_test_database()
    db.create_tables()
    db.insert_test_data()
    
    yield db
    
    # Teardown
    db.drop_tables()
    db.close()


def test_user_crud_integration(db_with_test_data):
    """Test CRUD avec DB réelle"""
    # Create
    user = db_with_test_data.create_user("alice", "alice@example.com")
    assert user.id is not None
    
    # Read
    retrieved = db_with_test_data.get_user(user.id)
    assert retrieved.username == "alice"
    
    # Update
    db_with_test_data.update_user(user.id, email="newemail@example.com")
    updated = db_with_test_data.get_user(user.id)
    assert updated.email == "newemail@example.com"
    
    # Delete
    db_with_test_data.delete_user(user.id)
    deleted = db_with_test_data.get_user(user.id)
    assert deleted is None


# ----------------------------------------------------------------------------
# [GRAPHIQUE] CODE COVERAGE
# ----------------------------------------------------------------------------

"""
CODE COVERAGE = Mesure du code testé

[IDEE] Qu'est-ce qui est mesuré ?
- Lignes exécutées
- Branches (if/else)
- Fonctions appelées


INSTALLATION

pip install pytest-cov


EXÉCUTION

pytest --cov=mon_module tests/
pytest --cov=mon_module --cov-report=html tests/


EXEMPLE
"""

# Fichier: calculator.py
class Calculator:
    def add(self, a, b):
        return a + b
    
    def divide(self, a, b):
        if b == 0:  # <- Branche 1
            raise ValueError("Division par zéro")
        return a / b  # <- Branche 2


# Fichier: test_calculator.py
def test_add():
    calc = Calculator()
    assert calc.add(2, 3) == 5


def test_divide():
    calc = Calculator()
    assert calc.divide(10, 2) == 5


"""
Exécution :
$ pytest --cov=calculator test_calculator.py

COVERAGE INCOMPLET !
- Ligne `divide` testée [OK]
- Branche `b == 0` PAS testée [X]


AJOUTER TEST POUR COUVRIR TOUT
"""

def test_divide_by_zero():
    """Test exception division par zéro"""
    calc = Calculator()
    with pytest.raises(ValueError):
        calc.divide(10, 0)


"""
Maintenant :
$ pytest --cov=calculator --cov-report=term-missing

Coverage : 100% [OK]


INTERPRÉTER COVERAGE

100% = Bon mais pas suffisant !
    Coverage mesure lignes exécutées
    Pas logique métier testée

80-90% = Raisonnable
    Bon équilibre

<70% = Insuffisant
    Beaucoup de code non testé


NE PAS VISER 100% À TOUT PRIX
- Se concentrer sur code critique
- Tests de qualité > % de coverage
"""


# ----------------------------------------------------------------------------
# [OK] BEST PRACTICES DE TESTING
# ----------------------------------------------------------------------------

"""
═══════════════════════════════════════════════════════════════════════════
BEST PRACTICES
═══════════════════════════════════════════════════════════════════════════


1⃣ NOMMAGE CLAIR
"""

# [X] Mauvais
def test1():
    pass

def test_user():
    pass

# [OK] Bon
def test_user_creation_avec_email_valide():
    """Nom descriptif"""
    pass

def test_retrait_leve_exception_si_solde_insuffisant():
    """On comprend ce qui est testé"""
    pass


"""
2⃣ UN TEST = UNE CHOSE
"""

# [X] Mauvais : teste plusieurs choses
def test_compte():
    compte = CompteBancaire("123", 1000)
    compte.deposer(500)
    assert compte.solde == 1500
    
    compte.retirer(200)
    assert compte.solde == 1300
    
    # Trop de logique dans un test !


# [OK] Bon : tests séparés
def test_depot():
    compte = CompteBancaire("123", 1000)
    compte.deposer(500)
    assert compte.solde == 1500


def test_retrait():
    compte = CompteBancaire("123", 1000)
    compte.retirer(200)
    assert compte.solde == 800


"""
3⃣ ARRANGE-ACT-ASSERT
"""

def test_avec_aaa():
    """Structure AAA claire"""
    # Arrange : Préparer
    compte = CompteBancaire("123", 1000)
    montant = 500
    
    # Act : Exécuter
    nouveau_solde = compte.deposer(montant)
    
    # Assert : Vérifier
    assert nouveau_solde == 1500
    assert compte.solde == 1500


"""
4⃣ TESTS INDÉPENDANTS
"""

# [X] Mauvais : tests dépendants
class TestDependants:
    compte = None
    
    def test_creation(self):
        self.compte = CompteBancaire("123", 1000)
    
    def test_depot(self):
        # [X] Dépend de test_creation
        self.compte.deposer(500)


# [OK] Bon : tests indépendants
class TestIndependants:
    @pytest.fixture
    def compte(self):
        return CompteBancaire("123", 1000)
    
    def test_creation(self):
        compte = CompteBancaire("123", 1000)
        assert compte.solde == 1000
    
    def test_depot(self, compte):
        # Indépendant, utilise fixture
        compte.deposer(500)
        assert compte.solde == 1500


"""
5⃣ TESTS RAPIDES
"""

# [X] Lent
def test_lent():
    import time
    time.sleep(5)  # [X] Trop lent
    assert True


# [OK] Rapide
def test_rapide():
    # Mock au lieu d'appel réel
    assert True


"""
6⃣ PAS DE LOGIQUE DANS TESTS
"""

# [X] Mauvais : logique complexe
def test_avec_logique():
    compte = CompteBancaire("123", 1000)
    
    for i in range(10):
        if i % 2 == 0:
            compte.deposer(100)
        else:
            compte.retirer(50)
    
    # Difficile de comprendre attendu
    assert compte.solde == ...


# [OK] Bon : simple et clair
def test_simple():
    compte = CompteBancaire("123", 1000)
    compte.deposer(100)
    compte.retirer(50)
    assert compte.solde == 1050


"""
7⃣ TESTER CAS LIMITES
"""

def test_cas_normaux():
    """Cas normaux"""
    compte = CompteBancaire("123", 1000)
    compte.deposer(500)
    assert compte.solde == 1500


def test_cas_limites():
    """Tester limites et edge cases"""
    compte = CompteBancaire("123", 0)
    
    # Limite : solde zéro
    compte.deposer(1)
    assert compte.solde == 1
    
    # Limite : retrait total
    compte.retirer(1)
    assert compte.solde == 0


def test_valeurs_extremes():
    """Valeurs extrêmes"""
    compte = CompteBancaire("123", 0)
    
    # Très grand montant
    compte.deposer(999999999)
    assert compte.solde == 999999999


"""
8⃣ MESSAGES D'ERREUR CLAIRS
"""

# [X] Mauvais
def test_mauvais_message():
    result = calculate_discount(100, "INVALID")
    assert result == 90  # Message d'erreur pas clair


# [OK] Bon
def test_bon_message():
    result = calculate_discount(100, "INVALID")
    assert result == 90, f"Code promo invalide devrait retourner prix original, obtenu {result}"


"""
9⃣ ÉVITER DÉPENDANCES EXTERNES DANS TESTS UNITAIRES
"""

# [X] Mauvais : appel réseau réel
def test_appel_api():
    response = requests.get("https://api.example.com/data")
    assert response.status_code == 200


# [OK] Bon : mock
@patch('requests.get')
def test_appel_api_mock(mock_get):
    mock_get.return_value.status_code = 200
    
    response = requests.get("https://api.example.com/data")
    assert response.status_code == 200


"""
[10] ORGANISER TESTS
"""

# Structure recommandée
"""
projet/
├── src/
│   ├── __init__.py
│   ├── calculator.py
│   └── user.py
└── tests/
    ├── __init__.py
    ├── conftest.py          # Fixtures partagées
    ├── test_calculator.py
    └── test_user.py
"""


"""
═══════════════════════════════════════════════════════════════════════════
CHECKLIST TESTING
═══════════════════════════════════════════════════════════════════════════

AVANT D'ÉCRIRE TEST :
[WHITE_SQUARE] Nom descriptif ?
[WHITE_SQUARE] Un seul concept testé ?
[WHITE_SQUARE] Structure AAA claire ?

PENDANT L'ÉCRITURE :
[WHITE_SQUARE] Pas de logique complexe ?
[WHITE_SQUARE] Pas de dépendances externes ?
[WHITE_SQUARE] Assertions claires ?

APRÈS L'ÉCRITURE :
[WHITE_SQUARE] Test indépendant ?
[WHITE_SQUARE] Test rapide ?
[WHITE_SQUARE] Cas limites couverts ?
[WHITE_SQUARE] Message d'erreur utile ?


═══════════════════════════════════════════════════════════════════════════
[COURS] RÉCAPITULATIF CHAPITRE 19
═══════════════════════════════════════════════════════════════════════════

VOUS MAÎTRISEZ MAINTENANT :

[OK] unittest (framework standard)
   - TestCase, setUp/tearDown
   - Assertions complètes
   - Test suites

[OK] pytest (framework moderne)
   - Assert natif Python
   - Fixtures puissantes
   - Paramétrage
   - Markers

[OK] Mocking
   - Mock/MagicMock
   - Patch temporaire
   - Side effects
   - Vérifications d'appels

[OK] TDD (Test Driven Development)
   - Cycle Red-Green-Refactor
   - Écrire tests d'abord
   - Design émergent

[OK] Tests d'intégration
   - Composants réels
   - Base de données
   - Fixtures complexes

[OK] Coverage
   - Mesure couverture
   - Interprétation
   - Limites

[OK] Best practices
   - Nommage
   - Organisation
   - Indépendance
   - Rapidité


COMPÉTENCES ACQUISES :

[OK] Tester classes et méthodes
[OK] Mocker dépendances externes
[OK] Pratiquer TDD
[OK] Mesurer qualité tests
[OK] Écrire tests maintenables
[OK] Tests unitaires et intégration


-> PROCHAINE ÉTAPE : CHAPITRE 20 - BEST PRACTICES GÉNÉRALES

Suite :
- Documentation professionnelle
- Organisation du code
- Performance et optimisation
- Anti-patterns à éviter
- Code review


Excellent travail ! Testing maîtrisé ! [TEST][OK]
"""

# ============================================================================
# [LIVRE] PYTHON POO - PARTIE 4 (FIN ULTIME) : ANTI-PATTERNS & RÉCAPITULATIF
# ============================================================================
#
# [OBJECTIF] CONTENU :
# - Fin Chapitre 20 : Anti-patterns, Code Review, Récapitulatif Final
#   * Anti-patterns courants à éviter
#   * Code review checklist
#   * Récapitulatif complet du guide
#   * Feuille de route pour aller plus loin
#
# [TEMPS] TEMPS : ~3-4 heures
# [DOCS] PRÉREQUIS : Tout le guide
# [IDEE] NIVEAU : Expert
# ============================================================================


# Suite et fin du Chapitre 20...


# ----------------------------------------------------------------------------
# [INTERDIT] ANTI-PATTERNS À ÉVITER
# ----------------------------------------------------------------------------

"""
═══════════════════════════════════════════════════════════════════════════
ANTI-PATTERNS POO COURANTS
═══════════════════════════════════════════════════════════════════════════

ANTI-PATTERN = Mauvaise pratique courante


1⃣ GOD OBJECT (OBJET DIEU)
═══════════════════════════════════════════════════════════════════════════
"""

# [X] ANTI-PATTERN : God Object

class ApplicationManager:
    """
    [X] GOD OBJECT
    
    Classe qui fait TOUT
    - Gère utilisateurs
    - Gère produits
    - Gère commandes
    - Envoie emails
    - Log
    - Base de données
    - Configuration
    
    -> Impossible à tester, maintenir, comprendre
    """
    
    def __init__(self):
        self.users = []
        self.products = []
        self.orders = []
        self.config = {}
        self.db_connection = None
    
    def create_user(self, name, email):
        """Créer utilisateur."""
        pass
    
    def send_email(self, to, subject, body):
        """Envoyer email."""
        pass
    
    def save_to_database(self, data):
        """Sauvegarder en DB."""
        pass
    
    def log(self, message):
        """Logger."""
        pass
    
    def process_order(self, order):
        """Traiter commande."""
        pass
    
    # ... 50 autres méthodes


# [OK] SOLUTION : Séparer responsabilités (SRP)

class UserService:
    """Service utilisateurs uniquement."""
    
    def create_user(self, name, email):
        pass


class EmailService:
    """Service email uniquement."""
    
    def send_email(self, to, subject, body):
        pass


class OrderService:
    """Service commandes uniquement."""
    
    def process_order(self, order):
        pass


class DatabaseService:
    """Service persistance uniquement."""
    
    def save(self, data):
        pass


class Logger:
    """Logger uniquement."""
    
    def log(self, message):
        pass


"""
2⃣ ANEMIC DOMAIN MODEL
═══════════════════════════════════════════════════════════════════════════
"""

# [X] ANTI-PATTERN : Modèle anémique (juste données, pas de comportement)

class User:
    """[X] Classe anémique : seulement getters/setters."""
    
    def __init__(self):
        self._name = ""
        self._email = ""
        self._is_active = False
    
    def get_name(self):
        return self._name
    
    def set_name(self, name):
        self._name = name
    
    def get_email(self):
        return self._email
    
    def set_email(self, email):
        self._email = email
    
    def get_is_active(self):
        return self._is_active
    
    def set_is_active(self, is_active):
        self._is_active = is_active


class UserService:
    """Toute la logique HORS du modèle."""
    
    def activate_user(self, user):
        user.set_is_active(True)
    
    def can_login(self, user):
        return user.get_is_active()


# [OK] SOLUTION : Modèle riche (avec comportement)

class User:
    """[OK] Modèle riche : données + comportement."""
    
    def __init__(self, name, email):
        self.name = name
        self.email = email
        self._is_active = False
    
    def activate(self):
        """Comportement métier dans le modèle."""
        self._is_active = True
    
    def can_login(self):
        """Logique métier dans le modèle."""
        return self._is_active and self.email
    
    def change_email(self, new_email):
        """Validation dans le modèle."""
        if '@' not in new_email:
            raise ValueError("Email invalide")
        self.email = new_email


"""
3⃣ TIGHT COUPLING (COUPLAGE FORT)
═══════════════════════════════════════════════════════════════════════════
"""

# [X] ANTI-PATTERN : Couplage fort

class OrderProcessor:
    """[X] Dépend directement de MySQLDatabase."""
    
    def __init__(self):
        self.db = MySQLDatabase()  # [X] Couplage fort
    
    def process(self, order):
        self.db.save(order)


# [OK] SOLUTION : Injection de dépendance (DIP)

class OrderProcessor:
    """[OK] Dépend d'abstraction."""
    
    def __init__(self, database: Database):  # [OK] Abstraction
        self.db = database
    
    def process(self, order):
        self.db.save(order)


# Flexible : peut utiliser n'importe quelle implémentation
processor = OrderProcessor(MySQLDatabase())
processor = OrderProcessor(PostgreSQLDatabase())
processor = OrderProcessor(MockDatabase())  # Tests


"""
4⃣ SPAGHETTI CODE
═══════════════════════════════════════════════════════════════════════════
"""

# [X] ANTI-PATTERN : Code spaghetti

class PaymentProcessor:
    """[X] Logique complexe mélangée."""
    
    def process_payment(self, amount, card_number, user_email, promo_code):
        if promo_code:
            if promo_code == "PROMO10":
                amount = amount * 0.9
            elif promo_code == "PROMO20":
                amount = amount * 0.8
        
        if card_number.startswith("4"):
            # Visa
            if amount > 1000:
                # Vérification spéciale
                if self._check_fraud(card_number):
                    return False
            self._charge_visa(card_number, amount)
        elif card_number.startswith("5"):
            # Mastercard
            if amount > 500:
                if self._check_fraud(card_number):
                    return False
            self._charge_mastercard(card_number, amount)
        
        # Envoyer email
        if user_email:
            # ... code email ...
            pass
        
        return True


# [OK] SOLUTION : Séparer responsabilités et patterns

class DiscountCalculator:
    """Calculer remises."""
    
    def apply_discount(self, amount, promo_code):
        discounts = {
            "PROMO10": 0.10,
            "PROMO20": 0.20
        }
        discount = discounts.get(promo_code, 0)
        return amount * (1 - discount)


class FraudDetector:
    """Détecter fraude."""
    
    def is_fraudulent(self, card_number, amount):
        # Logique détection
        return False


class PaymentGateway(ABC):
    """Interface paiement."""
    
    @abstractmethod
    def charge(self, card_number, amount):
        pass


class VisaGateway(PaymentGateway):
    """Gateway Visa."""
    
    def charge(self, card_number, amount):
        # Logique Visa
        pass


class MastercardGateway(PaymentGateway):
    """Gateway Mastercard."""
    
    def charge(self, card_number, amount):
        # Logique Mastercard
        pass


class PaymentProcessor:
    """[OK] Orchestration propre."""
    
    def __init__(
        self,
        discount_calculator: DiscountCalculator,
        fraud_detector: FraudDetector,
        email_service: EmailService
    ):
        self.discount_calculator = discount_calculator
        self.fraud_detector = fraud_detector
        self.email_service = email_service
        self.gateways = {
            '4': VisaGateway(),
            '5': MastercardGateway()
        }
    
    def process_payment(self, amount, card_number, user_email, promo_code):
        # 1. Appliquer remise
        final_amount = self.discount_calculator.apply_discount(amount, promo_code)
        
        # 2. Vérifier fraude
        if self.fraud_detector.is_fraudulent(card_number, final_amount):
            return False
        
        # 3. Traiter paiement
        gateway = self._get_gateway(card_number)
        gateway.charge(card_number, final_amount)
        
        # 4. Notifier
        self.email_service.send_confirmation(user_email, final_amount)
        
        return True
    
    def _get_gateway(self, card_number):
        first_digit = card_number[0]
        return self.gateways.get(first_digit)


"""
5⃣ CIRCULAR DEPENDENCIES
═══════════════════════════════════════════════════════════════════════════
"""

# [X] ANTI-PATTERN : Dépendances circulaires

class User:
    """[X] Dépend de Order."""
    
    def __init__(self):
        self.orders = []
    
    def add_order(self, order):
        self.orders.append(order)
        order.set_user(self)  # [X] Circulaire


class Order:
    """[X] Dépend de User."""
    
    def __init__(self):
        self.user = None
    
    def set_user(self, user):
        self.user = user
        user.add_order(self)  # [X] Circulaire


# [OK] SOLUTION : Introduire abstraction ou événements

class User:
    """[OK] Pas de référence directe à Order."""
    
    def __init__(self):
        self.order_ids = []  # Juste IDs


class Order:
    """[OK] Référence unidirectionnelle."""
    
    def __init__(self, user_id):
        self.user_id = user_id  # Référence par ID


# Ou utiliser pattern Repository
class OrderRepository:
    """Gérer relation."""
    
    def get_user_orders(self, user_id):
        # Récupérer commandes d'un utilisateur
        pass


"""
6⃣ PREMATURE OPTIMIZATION
═══════════════════════════════════════════════════════════════════════════
"""

# [X] ANTI-PATTERN : Optimisation prématurée

class DataProcessor:
    """[X] Complexe pour rien."""
    
    def __init__(self):
        # Optimisation prématurée
        self._cache = {}
        self._pool = ThreadPool(10)
        self._buffer = deque(maxlen=1000)
    
    def process(self, data):
        # Code complexe "optimisé" avant de savoir si nécessaire
        pass


# [OK] SOLUTION : Simple d'abord, optimiser si besoin

class DataProcessor:
    """[OK] Simple et clair."""
    
    def process(self, data):
        # Code simple qui fonctionne
        return [x * 2 for x in data]


# Optimiser SEULEMENT après profiling si besoin
# Si vraiment nécessaire :
class OptimizedDataProcessor:
    """Optimisé après mesure."""
    
    def __init__(self):
        self._cache = {}  # Ajouté après profiling
    
    def process(self, data):
        # Optimisation ciblée
        pass


"""
7⃣ SHOTGUN SURGERY
═══════════════════════════════════════════════════════════════════════════
"""

# [X] ANTI-PATTERN : Modification nécessite changements partout

# Pour ajouter champ "phone" à User :
# 1. Modifier User
# 2. Modifier UserDTO
# 3. Modifier UserForm
# 4. Modifier UserValidator
# 5. Modifier UserRepository
# 6. Modifier UserService
# 7. Modifier tous les tests
# -> Changement dans 10+ fichiers !


# [OK] SOLUTION : Encapsulation et abstraction

class User:
    """Données encapsulées."""
    
    def __init__(self, **kwargs):
        self._data = kwargs
    
    def get(self, key):
        return self._data.get(key)
    
    def set(self, key, value):
        self._data[key] = value


# Ajouter "phone" : modification localisée


"""
═══════════════════════════════════════════════════════════════════════════
AUTRES ANTI-PATTERNS COURANTS
═══════════════════════════════════════════════════════════════════════════

8⃣ FEATURE ENVY : Méthode utilise plus données d'autre classe que les siennes

9⃣ DATA CLUMPS : Mêmes données toujours ensemble -> créer classe

[10] LONG PARAMETER LIST : Trop de paramètres -> objet de configuration

1⃣1⃣ SWITCH STATEMENTS : Beaucoup de if/elif -> polymorphisme

1⃣2⃣ LAZY CLASS : Classe ne fait presque rien -> fusionner

1⃣3⃣ SPECULATIVE GENERALITY : Code pour besoins futurs hypothétiques
"""


# ----------------------------------------------------------------------------
# [OK] CODE REVIEW CHECKLIST
# ----------------------------------------------------------------------------

"""
═══════════════════════════════════════════════════════════════════════════
CHECKLIST CODE REVIEW POO
═══════════════════════════════════════════════════════════════════════════


[LISTE] DESIGN PRINCIPES SOLID
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] SRP : Chaque classe a UNE responsabilité ?
[WHITE_SQUARE] OCP : Extensible sans modification ?
[WHITE_SQUARE] LSP : Sous-classes substituables ?
[WHITE_SQUARE] ISP : Interfaces ciblées (pas trop larges) ?
[WHITE_SQUARE] DIP : Dépend d'abstractions, pas de concrétions ?


[LISTE] NOMMAGE
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] Noms de classes : PascalCase, noms clairs ?
[WHITE_SQUARE] Noms de méthodes : snake_case, verbes d'action ?
[WHITE_SQUARE] Noms de variables : descriptifs, pas d'abréviations ?
[WHITE_SQUARE] Constantes : UPPER_CASE ?
[WHITE_SQUARE] Variables privées : _prefixe ?


[LISTE] DOCUMENTATION
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] Docstrings pour classes publiques ?
[WHITE_SQUARE] Docstrings pour méthodes publiques ?
[WHITE_SQUARE] Type hints présents ?
[WHITE_SQUARE] Commentaires expliquent POURQUOI, pas QUOI ?
[WHITE_SQUARE] README à jour ?


[LISTE] TESTS
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] Tests unitaires pour nouvelle fonctionnalité ?
[WHITE_SQUARE] Tests passent tous ?
[WHITE_SQUARE] Coverage acceptable (>80%) ?
[WHITE_SQUARE] Tests indépendants ?
[WHITE_SQUARE] Noms de tests descriptifs ?
[WHITE_SQUARE] Mocks pour dépendances externes ?


[LISTE] STRUCTURE ET ORGANISATION
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] Fichiers organisés logiquement ?
[WHITE_SQUARE] Imports propres (pas d'imports inutilisés) ?
[WHITE_SQUARE] Pas de code dupliqué ?
[WHITE_SQUARE] Fonctions/méthodes < 50 lignes ?
[WHITE_SQUARE] Classes < 300 lignes ?
[WHITE_SQUARE] Séparation domaine/infrastructure ?


[LISTE] GESTION D'ERREURS
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] Exceptions appropriées levées ?
[WHITE_SQUARE] Pas de bare except ?
[WHITE_SQUARE] Messages d'erreur clairs ?
[WHITE_SQUARE] Ressources nettoyées (context managers) ?


[LISTE] PERFORMANCE
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] Pas d'optimisation prématurée ?
[WHITE_SQUARE] Algorithmes appropriés ?
[WHITE_SQUARE] Pas de calculs redondants ?
[WHITE_SQUARE] Resources libérées correctement ?


[LISTE] SÉCURITÉ
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] Validation des entrées ?
[WHITE_SQUARE] Pas de secrets en dur ?
[WHITE_SQUARE] Injection SQL évitée ?
[WHITE_SQUARE] Permissions vérifiées ?


[LISTE] DESIGN PATTERNS
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] Patterns appropriés utilisés ?
[WHITE_SQUARE] Pas de sur-engineering ?
[WHITE_SQUARE] Abstractions justifiées ?


[LISTE] ANTI-PATTERNS
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] Pas de God Object ?
[WHITE_SQUARE] Pas de code anémique ?
[WHITE_SQUARE] Pas de couplage fort ?
[WHITE_SQUARE] Pas de dépendances circulaires ?
[WHITE_SQUARE] Pas de shotgun surgery ?


[LISTE] BONNES PRATIQUES PYTHON
═══════════════════════════════════════════════════════════════════════════

[WHITE_SQUARE] PEP 8 respecté ?
[WHITE_SQUARE] Compréhensions de liste appropriées ?
[WHITE_SQUARE] Context managers utilisés ?
[WHITE_SQUARE] Generators si applicable ?
[WHITE_SQUARE] Decorators appropriés ?


TEMPLATE CODE REVIEW
═══════════════════════════════════════════════════════════════════════════

# Commentaire de review

## Positif [OK]
- Code bien structuré
- Tests complets
- Documentation claire

## À améliorer [OUTIL]
- [ ] Extraire méthode `process()` (trop longue)
- [ ] Ajouter type hints
- [ ] Renommer `x` en `user_count`

## Questions [?]
- Pourquoi choix de X plutôt que Y ?
- Performance testée avec gros volume ?

## Bloquant [INTERDIT]
- Pas de tests pour nouvelle fonctionnalité
- Violation SRP dans `UserManager`
"""


# ============================================================================
# [COURS] RÉCAPITULATIF FINAL COMPLET DU GUIDE
# ============================================================================

"""
═══════════════════════════════════════════════════════════════════════════
[BRAVO] FÉLICITATIONS ! GUIDE COMPLET TERMINÉ !
═══════════════════════════════════════════════════════════════════════════


VOUS AVEZ PARCOURU :
───────────────────────────────────────────────────────────────────────────

[DOCS] 4 PARTIES COMPLÈTES
[FICHIER] 20 CHAPITRES DÉTAILLÉS
[GUIDE] 30 000+ LIGNES DE CODE ET EXPLICATIONS
[OBJECTIF] 150+ EXEMPLES PRATIQUES
[TEST] 50+ EXERCICES AVEC SOLUTIONS
[TEMPS] 60+ HEURES DE CONTENU


═══════════════════════════════════════════════════════════════════════════
PARTIE 1 : FONDAMENTAUX (Chapitres 0-4)
═══════════════════════════════════════════════════════════════════════════

[OK] Chapitre 0 : Introduction à la POO
   - Paradigmes de programmation
   - Concepts clés (encapsulation, héritage, polymorphisme)
   - POO vs Procédural

[OK] Chapitre 1 : Premières Classes
   - Définir classes et objets
   - self et instances
   - Attributs d'instance et de classe

[OK] Chapitre 2 : Attributs et Méthodes
   - Méthodes d'instance
   - @classmethod et @staticmethod
   - @property (getters/setters)

[OK] Chapitre 3 : Constructeur Avancé
   - __init__ avec *args et **kwargs
   - Valeurs par défaut
   - Validation dans constructeur

[OK] Chapitre 4 : Méthodes Spéciales (Magic Methods)
   - __str__ et __repr__
   - Opérateurs (__add__, __eq__, etc.)
   - __len__, __getitem__ (collections)
   - __iter__, __next__ (itérateurs)
   - __enter__, __exit__ (context managers)


═══════════════════════════════════════════════════════════════════════════
PARTIE 2 : HÉRITAGE ET POLYMORPHISME (Chapitres 5-8)
═══════════════════════════════════════════════════════════════════════════

[OK] Chapitre 5 : Héritage Simple
   - Syntaxe héritage
   - super() et MRO
   - Surcharge de méthodes
   - isinstance() et issubclass()

[OK] Chapitre 6 : Héritage Multiple
   - Syntaxe multi-héritage
   - MRO et C3 Linearization
   - Problème du diamant
   - Mix-ins

[OK] Chapitre 7 : Polymorphisme
   - Polymorphisme par héritage
   - Duck typing
   - Protocoles Python
   - Interfaces implicites

[OK] Chapitre 8 : Classes Abstraites (ABC)
   - Module abc
   - @abstractmethod
   - Propriétés abstraites
   - Interfaces avec ABC


═══════════════════════════════════════════════════════════════════════════
PARTIE 3 : CONCEPTS AVANCÉS (Chapitres 9-13)
═══════════════════════════════════════════════════════════════════════════

[OK] Chapitre 9 : Encapsulation Avancée
   - Niveaux d'accès (public, _protégé, __privé)
   - Name mangling
   - Properties avancées avec cache
   - __slots__ pour optimisation
   - Immutabilité

[OK] Chapitre 10 : Dataclasses
   - @dataclass
   - field() et default_factory
   - frozen=True
   - __post_init__
   - order=True

[OK] Chapitre 11 : Composition vs Héritage
   - Différence "EST-UN" vs "A-UN"
   - Problèmes de l'héritage
   - Avantages composition
   - Injection de dépendances
   - Patterns de composition

[OK] Chapitre 12 : Descripteurs
   - Protocole descripteur (__get__, __set__, __delete__)
   - Data vs Non-data descriptors
   - __set_name__
   - Descripteurs de validation
   - Fonctionnement de @property

[OK] Chapitre 13 : Métaclasses
   - Classes sont instances de type
   - Créer classes avec type()
   - Métaclasses personnalisées
   - __new__ vs __init__
   - Patterns : Registry, Singleton
   - __init_subclass__ (alternative)


═══════════════════════════════════════════════════════════════════════════
PARTIE 4 : DESIGN PATTERNS & BEST PRACTICES (Chapitres 14-20)
═══════════════════════════════════════════════════════════════════════════

[OK] Chapitre 14 : Introduction Design Patterns
   - Qu'est-ce qu'un pattern
   - Gang of Four (GoF)
   - 3 catégories de patterns

[OK] Chapitre 15 : Patterns Créationnels
   - Singleton (instance unique)
   - Factory Method (création flexible)
   - Abstract Factory (familles d'objets)
   - Builder (construction étape par étape)
   - Prototype (clonage)

[OK] Chapitre 16 : Patterns Structurels
   - Adapter (compatibilité interfaces)
   - Decorator (ajout dynamique)
   - Facade (simplification)
   - Proxy (lazy loading, cache, protection)
   - Composite (structures arborescentes)

[OK] Chapitre 17 : Patterns Comportementaux
   - Observer (pub/sub, événements)
   - Strategy (algorithmes interchangeables)
   - Command (encapsulation actions, undo/redo)
   - State (gestion d'états)
   - Template Method (squelette algorithme)
   - Iterator (parcours collections)
   - Chain of Responsibility (chaîne traitement)

[OK] Chapitre 18 : Principes SOLID
   - Single Responsibility Principle
   - Open/Closed Principle
   - Liskov Substitution Principle
   - Interface Segregation Principle
   - Dependency Inversion Principle

[OK] Chapitre 19 : Testing Orienté Objet
   - unittest (framework standard)
   - pytest (framework moderne)
   - Mocking (Mock, patch, side_effect)
   - TDD (Red-Green-Refactor)
   - Tests d'intégration
   - Coverage
   - Best practices testing

[OK] Chapitre 20 : Best Practices Professionnelles
   - Documentation (docstrings, type hints, Sphinx)
   - Organisation code (structure projets)
   - Performance (profiling, optimisation)
   - Anti-patterns (God Object, code anémique, etc.)
   - Code review checklist


═══════════════════════════════════════════════════════════════════════════
[OBJECTIF] COMPÉTENCES MAÎTRISÉES
═══════════════════════════════════════════════════════════════════════════

FONDAMENTAUX :
[OK] Créer classes et objets
[OK] Utiliser attributs et méthodes
[OK] Implémenter magic methods
[OK] Créer properties

HÉRITAGE :
[OK] Utiliser héritage simple et multiple
[OK] Comprendre MRO
[OK] Appliquer polymorphisme
[OK] Créer classes abstraites

CONCEPTS AVANCÉS :
[OK] Maîtriser encapsulation
[OK] Utiliser dataclasses
[OK] Choisir composition vs héritage
[OK] Créer descripteurs
[OK] Utiliser métaclasses (quand nécessaire)

DESIGN PATTERNS :
[OK] Appliquer 15+ patterns
[OK] Résoudre problèmes récurrents
[OK] Concevoir architectures solides

PRINCIPES :
[OK] Appliquer SOLID
[OK] Identifier violations
[OK] Refactorer code

TESTING :
[OK] Écrire tests unitaires et intégration
[OK] Utiliser mocks
[OK] Pratiquer TDD
[OK] Mesurer coverage

PROFESSIONNALISME :
[OK] Documenter professionnellement
[OK] Organiser projets
[OK] Optimiser performances
[OK] Éviter anti-patterns
[OK] Faire code reviews


═══════════════════════════════════════════════════════════════════════════
[RAPIDE] POUR ALLER PLUS LOIN
═══════════════════════════════════════════════════════════════════════════

APPROFONDIR :

1. Domain-Driven Design (DDD)
   - Bounded Contexts
   - Aggregates
   - Value Objects
   - Domain Events

2. Architecture Hexagonale (Ports & Adapters)
   - Separation core/infrastructure
   - Testabilité maximale

3. CQRS (Command Query Responsibility Segregation)
   - Séparer lectures et écritures

4. Event Sourcing
   - Stocker événements, pas états

5. Microservices avec POO
   - Services découplés
   - Communication inter-services


FRAMEWORKS À EXPLORER :

- Django (web framework POO)
- FastAPI (API moderne avec type hints)
- SQLAlchemy (ORM Python)
- Pydantic (validation données)


LECTURES RECOMMANDÉES :

[DOCS] "Clean Code" - Robert C. Martin
[DOCS] "Design Patterns" - Gang of Four
[DOCS] "Refactoring" - Martin Fowler
[DOCS] "Domain-Driven Design" - Eric Evans
[DOCS] "Clean Architecture" - Robert C. Martin


PRATIQUE :

[OK] Contribuer à projets open source
[OK] Créer projets personnels
[OK] Code katas (exercices)
[OK] Pair programming
[OK] Code reviews


═══════════════════════════════════════════════════════════════════════════
[GEM_STONE] DERNIERS CONSEILS
═══════════════════════════════════════════════════════════════════════════

1. SIMPLICITÉ D'ABORD
   "Simple is better than complex"
   Ne pas over-engineer

2. PRAGMATISME
   Appliquer principes avec discernement
   Pas de dogmatisme

3. PRACTICE MAKES PERFECT
   Coder régulièrement
   Apprendre de ses erreurs

4. CODE REVIEW
   Lire code des autres
   Faire reviewer son code

5. RESTER À JOUR
   Python évolue
   Nouveaux patterns émergent

6. COMMUNAUTÉ
   Participer forums
   Partager connaissances

7. TESTS, TESTS, TESTS
   Code testé = code fiable
   TDD change la donne

8. REFACTORING CONTINU
   Code se dégrade
   Améliorer régulièrement


═══════════════════════════════════════════════════════════════════════════
[TROPHEE] VOUS ÊTES MAINTENANT UN EXPERT POO PYTHON !
═══════════════════════════════════════════════════════════════════════════

Vous avez parcouru un guide COMPLET et ULTRA-DÉTAILLÉ de la POO en Python.

Vous maîtrisez :
[OK] Tous les fondamentaux
[OK] Tous les concepts avancés
[OK] 15+ design patterns
[OK] 5 principes SOLID
[OK] Testing professionnel
[OK] Best practices

Vous êtes capable de :
[OK] Concevoir architectures complexes
[OK] Écrire code maintenable
[OK] Appliquer bonnes pratiques
[OK] Résoudre problèmes réels
[OK] Travailler professionnellement


═══════════════════════════════════════════════════════════════════════════
FÉLICITATIONS ET BONNE CONTINUATION ! [BRAVO][RAPIDE]
═══════════════════════════════════════════════════════════════════════════

Merci d'avoir suivi ce guide jusqu'au bout !

N'oubliez pas : La maîtrise vient avec la pratique.
Continuez à coder, à apprendre, et à vous améliorer.

Happy Coding! [CODE]*
"""

# ============================================================================
# [SHOPPING_TROLLEY] PROJET PRATIQUE 1 : SYSTÈME E-COMMERCE COMPLET
# ============================================================================
#
# [OBJECTIF] OBJECTIF :
# Créer un système e-commerce professionnel appliquant TOUS les concepts POO :
# - Design Patterns (Singleton, Factory, Observer, Strategy, Command, etc.)
# - Principes SOLID
# - Tests complets
# - Architecture propre
#
# [DOCS] CONCEPTS APPLIQUÉS :
# [OK] 10+ Design Patterns
# [OK] Principes SOLID
# [OK] Tests unitaires et d'intégration
# [OK] Documentation professionnelle
# [OK] Architecture en couches
#
# [TEMPS] TEMPS : ~8-10 heures de lecture et pratique
# [IDEE] NIVEAU : Application des connaissances acquises
# ============================================================================


"""
═══════════════════════════════════════════════════════════════════════════
[WORLD_MAP] ARCHITECTURE DU PROJET
═══════════════════════════════════════════════════════════════════════════

ecommerce/
├── domain/                 # Domaine métier (entités, logique)
│   ├── models/            # Modèles domaine
│   │   ├── product.py
│   │   ├── user.py
│   │   ├── order.py
│   │   └── cart.py
│   ├── services/          # Services métier
│   │   ├── order_service.py
│   │   ├── cart_service.py
│   │   └── payment_service.py
│   └── interfaces/        # Interfaces (ports)
│       ├── repository.py
│       └── notification.py
├── infrastructure/        # Implémentations techniques
│   ├── repositories/      # Persistance
│   ├── notifications/     # Email, SMS
│   └── payment/          # Passerelles paiement
├── application/          # Cas d'usage
│   └── use_cases/
└── tests/               # Tests complets
    ├── unit/
    └── integration/


PATTERNS UTILISÉS :

[OK] Singleton         -> Configuration globale
[OK] Factory Method    -> Création produits
[OK] Abstract Factory  -> Famille notifications
[OK] Builder          -> Construction commandes complexes
[OK] Prototype        -> Clonage produits
[OK] Adapter          -> APIs paiement externes
[OK] Decorator        -> Ajout fonctionnalités produits
[OK] Facade           -> Interface simple checkout
[OK] Proxy            -> Lazy loading produits
[OK] Observer         -> Notifications événements
[OK] Strategy         -> Stratégies prix/livraison
[OK] Command          -> Actions annulables
[OK] State            -> États commande
[OK] Template Method  -> Process commande
"""


# ============================================================================
# [PACKAGE] PARTIE 1 : DOMAINE MÉTIER (MODELS)
# ============================================================================

from __future__ import annotations
from typing import List, Optional, Dict, Protocol
from datetime import datetime
from decimal import Decimal
from enum import Enum
from abc import ABC, abstractmethod
import uuid


"""
═══════════════════════════════════════════════════════════════════════════
MODÈLE 1 : PRODUIT
═══════════════════════════════════════════════════════════════════════════

Concepts appliqués :
- Encapsulation
- Validation
- Properties
- Immutabilité partielle
"""

class ProductCategory(Enum):
    """Catégories de produits."""
    ELECTRONICS = "electronics"
    CLOTHING = "clothing"
    BOOKS = "books"
    HOME = "home"
    FOOD = "food"


class Product:
    """
    Produit du catalogue.
    
    Représente un produit avec ses caractéristiques,
    prix, stock et métadonnées.
    
    Attributes:
        id: Identifiant unique
        name: Nom du produit
        description: Description détaillée
        price: Prix unitaire
        category: Catégorie
        stock: Quantité en stock
        created_at: Date de création
        
    Example:
        >>> product = Product(
        ...     name="Laptop Dell XPS",
        ...     price=Decimal("999.99"),
        ...     category=ProductCategory.ELECTRONICS,
        ...     stock=10
        ... )
        >>> print(product.name)
        Laptop Dell XPS
    """
    
    def __init__(
        self,
        name: str,
        price: Decimal,
        category: ProductCategory,
        stock: int = 0,
        description: str = "",
        product_id: Optional[str] = None
    ):
        """
        Initialise un produit.
        
        Args:
            name: Nom du produit
            price: Prix unitaire (Decimal pour précision)
            category: Catégorie du produit
            stock: Quantité initiale en stock
            description: Description du produit
            product_id: ID personnalisé (auto-généré si None)
            
        Raises:
            ValueError: Si prix négatif ou stock négatif
        """
        # Validation
        if price < 0:
            raise ValueError("Prix ne peut pas être négatif")
        if stock < 0:
            raise ValueError("Stock ne peut pas être négatif")
        if not name.strip():
            raise ValueError("Nom ne peut pas être vide")
        
        # Attributs publics (lecture seule via properties)
        self._id = product_id or str(uuid.uuid4())
        self._name = name.strip()
        self._description = description.strip()
        self._price = price
        self._category = category
        self._stock = stock
        self._created_at = datetime.now()
        self._updated_at = datetime.now()
    
    # Properties (lecture seule pour certains attributs)
    @property
    def id(self) -> str:
        """ID du produit (lecture seule)."""
        return self._id
    
    @property
    def name(self) -> str:
        """Nom du produit."""
        return self._name
    
    @property
    def description(self) -> str:
        """Description du produit."""
        return self._description
    
    @property
    def price(self) -> Decimal:
        """Prix unitaire."""
        return self._price
    
    @price.setter
    def price(self, value: Decimal) -> None:
        """
        Modifier prix.
        
        Args:
            value: Nouveau prix
            
        Raises:
            ValueError: Si prix négatif
        """
        if value < 0:
            raise ValueError("Prix ne peut pas être négatif")
        self._price = value
        self._updated_at = datetime.now()
    
    @property
    def category(self) -> ProductCategory:
        """Catégorie du produit."""
        return self._category
    
    @property
    def stock(self) -> int:
        """Quantité en stock."""
        return self._stock
    
    @property
    def created_at(self) -> datetime:
        """Date de création."""
        return self._created_at
    
    @property
    def updated_at(self) -> datetime:
        """Date de dernière modification."""
        return self._updated_at
    
    # Méthodes métier
    def is_available(self) -> bool:
        """
        Vérifier disponibilité.
        
        Returns:
            True si produit en stock
        """
        return self._stock > 0
    
    def has_sufficient_stock(self, quantity: int) -> bool:
        """
        Vérifier stock suffisant.
        
        Args:
            quantity: Quantité demandée
            
        Returns:
            True si stock suffisant
        """
        return self._stock >= quantity
    
    def decrease_stock(self, quantity: int) -> None:
        """
        Diminuer stock.
        
        Args:
            quantity: Quantité à retirer
            
        Raises:
            ValueError: Si stock insuffisant
        """
        if not self.has_sufficient_stock(quantity):
            raise ValueError(
                f"Stock insuffisant. Disponible: {self._stock}, "
                f"Demandé: {quantity}"
            )
        self._stock -= quantity
        self._updated_at = datetime.now()
    
    def increase_stock(self, quantity: int) -> None:
        """
        Augmenter stock.
        
        Args:
            quantity: Quantité à ajouter
            
        Raises:
            ValueError: Si quantité négative
        """
        if quantity < 0:
            raise ValueError("Quantité doit être positive")
        self._stock += quantity
        self._updated_at = datetime.now()
    
    def __repr__(self) -> str:
        """Représentation développeur."""
        return (
            f"Product(id={self.id!r}, name={self.name!r}, "
            f"price={self.price}, stock={self.stock})"
        )
    
    def __str__(self) -> str:
        """Représentation utilisateur."""
        return f"{self.name} - {self.price}€ (Stock: {self.stock})"
    
    def __eq__(self, other) -> bool:
        """Égalité basée sur ID."""
        if not isinstance(other, Product):
            return False
        return self.id == other.id
    
    def __hash__(self) -> int:
        """Hash basé sur ID."""
        return hash(self.id)


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN : PROTOTYPE (Clonage de produits)
═══════════════════════════════════════════════════════════════════════════
"""

class ProductPrototype(Product):
    """
    [IDEE] PATTERN PROTOTYPE
    
    Permet de cloner un produit existant pour créer variantes.
    """
    
    def clone(self, **overrides) -> Product:
        """
        Cloner produit avec modifications optionnelles.
        
        Args:
            **overrides: Attributs à modifier dans le clone
            
        Returns:
            Nouveau produit cloné
            
        Example:
            >>> original = ProductPrototype(
            ...     name="T-Shirt Rouge",
            ...     price=Decimal("19.99"),
            ...     category=ProductCategory.CLOTHING,
            ...     stock=50
            ... )
            >>> blue_variant = original.clone(
            ...     name="T-Shirt Bleu",
            ...     product_id=None  # Nouvel ID
            ... )
        """
        return Product(
            name=overrides.get('name', self.name),
            price=overrides.get('price', self.price),
            category=overrides.get('category', self.category),
            stock=overrides.get('stock', self.stock),
            description=overrides.get('description', self.description),
            product_id=overrides.get('product_id')  # None = nouvel ID
        )


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN : DECORATOR (Fonctionnalités produit)
═══════════════════════════════════════════════════════════════════════════
"""

class ProductDecorator(ABC):
    """
    [IDEE] PATTERN DECORATOR
    
    Ajouter fonctionnalités à un produit dynamiquement.
    """
    
    def __init__(self, product: Product):
        """
        Initialise décorateur.
        
        Args:
            product: Produit à décorer
        """
        self._product = product
    
    @property
    def price(self) -> Decimal:
        """Prix du produit (peut être modifié par décorateur)."""
        return self._product.price
    
    def __getattr__(self, name):
        """Déléguer autres attributs au produit."""
        return getattr(self._product, name)


class GiftWrapDecorator(ProductDecorator):
    """
    Décorateur : Emballage cadeau.
    
    Ajoute frais d'emballage au prix.
    """
    
    GIFT_WRAP_COST = Decimal("2.99")
    
    @property
    def price(self) -> Decimal:
        """Prix avec emballage."""
        return self._product.price + self.GIFT_WRAP_COST
    
    def __str__(self) -> str:
        return f"{self._product.name} (avec emballage cadeau) - {self.price}€"


class ExpressShippingDecorator(ProductDecorator):
    """
    Décorateur : Livraison express.
    
    Ajoute frais de livraison express.
    """
    
    EXPRESS_COST = Decimal("9.99")
    
    @property
    def price(self) -> Decimal:
        """Prix avec livraison express."""
        return self._product.price + self.EXPRESS_COST
    
    def __str__(self) -> str:
        return f"{self._product.name} (livraison express) - {self.price}€"


class WarrantyDecorator(ProductDecorator):
    """
    Décorateur : Garantie étendue.
    
    Ajoute frais de garantie.
    """
    
    def __init__(self, product: Product, years: int = 2):
        """
        Initialise garantie.
        
        Args:
            product: Produit à garantir
            years: Années de garantie
        """
        super().__init__(product)
        self.years = years
        self.warranty_cost = Decimal("29.99") * years
    
    @property
    def price(self) -> Decimal:
        """Prix avec garantie."""
        return self._product.price + self.warranty_cost
    
    def __str__(self) -> str:
        return f"{self._product.name} (garantie {self.years} ans) - {self.price}€"


"""
═══════════════════════════════════════════════════════════════════════════
MODÈLE 2 : UTILISATEUR
═══════════════════════════════════════════════════════════════════════════
"""

class User:
    """
    Utilisateur du système.
    
    Attributes:
        id: Identifiant unique
        email: Email (unique)
        name: Nom complet
        password_hash: Hash du mot de passe
        is_active: Compte actif ou non
        created_at: Date d'inscription
    """
    
    def __init__(
        self,
        email: str,
        name: str,
        password_hash: str,
        user_id: Optional[str] = None
    ):
        """
        Initialise utilisateur.
        
        Args:
            email: Email unique
            name: Nom complet
            password_hash: Hash du mot de passe (jamais en clair !)
            user_id: ID personnalisé (auto-généré si None)
            
        Raises:
            ValueError: Si email invalide
        """
        # Validation email
        if '@' not in email or '.' not in email:
            raise ValueError("Email invalide")
        
        if not name.strip():
            raise ValueError("Nom ne peut pas être vide")
        
        self._id = user_id or str(uuid.uuid4())
        self._email = email.lower().strip()
        self._name = name.strip()
        self._password_hash = password_hash
        self._is_active = True
        self._created_at = datetime.now()
    
    @property
    def id(self) -> str:
        """ID utilisateur."""
        return self._id
    
    @property
    def email(self) -> str:
        """Email."""
        return self._email
    
    @property
    def name(self) -> str:
        """Nom."""
        return self._name
    
    @property
    def password_hash(self) -> str:
        """Hash mot de passe."""
        return self._password_hash
    
    @property
    def is_active(self) -> bool:
        """Compte actif."""
        return self._is_active
    
    @property
    def created_at(self) -> datetime:
        """Date création."""
        return self._created_at
    
    def activate(self) -> None:
        """Activer compte."""
        self._is_active = True
    
    def deactivate(self) -> None:
        """Désactiver compte."""
        self._is_active = False
    
    def change_password(self, new_password_hash: str) -> None:
        """
        Changer mot de passe.
        
        Args:
            new_password_hash: Nouveau hash
        """
        self._password_hash = new_password_hash
    
    def __repr__(self) -> str:
        return f"User(id={self.id!r}, email={self.email!r}, name={self.name!r})"
    
    def __str__(self) -> str:
        return f"{self.name} ({self.email})"
    
    def __eq__(self, other) -> bool:
        if not isinstance(other, User):
            return False
        return self.id == other.id
    
    def __hash__(self) -> int:
        return hash(self.id)


"""
═══════════════════════════════════════════════════════════════════════════
MODÈLE 3 : PANIER D'ACHAT
═══════════════════════════════════════════════════════════════════════════
"""

class CartItem:
    """
    Item du panier.
    
    Représente un produit avec sa quantité dans le panier.
    """
    
    def __init__(self, product: Product, quantity: int = 1):
        """
        Initialise item panier.
        
        Args:
            product: Produit
            quantity: Quantité
            
        Raises:
            ValueError: Si quantité invalide
        """
        if quantity <= 0:
            raise ValueError("Quantité doit être positive")
        
        self._product = product
        self._quantity = quantity
    
    @property
    def product(self) -> Product:
        """Produit."""
        return self._product
    
    @property
    def quantity(self) -> int:
        """Quantité."""
        return self._quantity
    
    @quantity.setter
    def quantity(self, value: int) -> None:
        """
        Modifier quantité.
        
        Args:
            value: Nouvelle quantité
            
        Raises:
            ValueError: Si quantité invalide
        """
        if value <= 0:
            raise ValueError("Quantité doit être positive")
        self._quantity = value
    
    def subtotal(self) -> Decimal:
        """
        Calculer sous-total.
        
        Returns:
            Prix × quantité
        """
        return self._product.price * self._quantity
    
    def __repr__(self) -> str:
        return f"CartItem(product={self._product.name}, quantity={self._quantity})"
    
    def __str__(self) -> str:
        return f"{self._product.name} x{self._quantity} = {self.subtotal()}€"


class ShoppingCart:
    """
    Panier d'achat.
    
    Gère les items du panier d'un utilisateur.
    """
    
    def __init__(self, user: User):
        """
        Initialise panier.
        
        Args:
            user: Utilisateur propriétaire
        """
        self._user = user
        self._items: Dict[str, CartItem] = {}  # product_id -> CartItem
        self._created_at = datetime.now()
    
    @property
    def user(self) -> User:
        """Utilisateur propriétaire."""
        return self._user
    
    @property
    def items(self) -> List[CartItem]:
        """Liste des items."""
        return list(self._items.values())
    
    @property
    def item_count(self) -> int:
        """Nombre d'items (différents produits)."""
        return len(self._items)
    
    @property
    def total_quantity(self) -> int:
        """Quantité totale (somme des quantités)."""
        return sum(item.quantity for item in self._items.values())
    
    def is_empty(self) -> bool:
        """Vérifier si panier vide."""
        return len(self._items) == 0
    
    def add_item(self, product: Product, quantity: int = 1) -> None:
        """
        Ajouter produit au panier.
        
        Args:
            product: Produit à ajouter
            quantity: Quantité à ajouter
            
        Raises:
            ValueError: Si quantité invalide ou stock insuffisant
        """
        if quantity <= 0:
            raise ValueError("Quantité doit être positive")
        
        # Vérifier stock
        if not product.has_sufficient_stock(quantity):
            raise ValueError(f"Stock insuffisant pour {product.name}")
        
        # Si produit déjà dans panier, augmenter quantité
        if product.id in self._items:
            new_quantity = self._items[product.id].quantity + quantity
            
            # Vérifier stock pour nouvelle quantité
            if not product.has_sufficient_stock(new_quantity):
                raise ValueError(f"Stock insuffisant pour {product.name}")
            
            self._items[product.id].quantity = new_quantity
        else:
            # Nouveau produit
            self._items[product.id] = CartItem(product, quantity)
    
    def remove_item(self, product_id: str) -> None:
        """
        Retirer produit du panier.
        
        Args:
            product_id: ID du produit à retirer
            
        Raises:
            KeyError: Si produit pas dans panier
        """
        if product_id not in self._items:
            raise KeyError(f"Produit {product_id} pas dans panier")
        
        del self._items[product_id]
    
    def update_quantity(self, product_id: str, quantity: int) -> None:
        """
        Modifier quantité d'un produit.
        
        Args:
            product_id: ID du produit
            quantity: Nouvelle quantité
            
        Raises:
            KeyError: Si produit pas dans panier
            ValueError: Si quantité invalide ou stock insuffisant
        """
        if product_id not in self._items:
            raise KeyError(f"Produit {product_id} pas dans panier")
        
        if quantity <= 0:
            raise ValueError("Quantité doit être positive")
        
        item = self._items[product_id]
        
        # Vérifier stock
        if not item.product.has_sufficient_stock(quantity):
            raise ValueError(f"Stock insuffisant pour {item.product.name}")
        
        item.quantity = quantity
    
    def clear(self) -> None:
        """Vider panier complètement."""
        self._items.clear()
    
    def calculate_total(self) -> Decimal:
        """
        Calculer total du panier.
        
        Returns:
            Somme de tous les sous-totaux
        """
        return sum(
            item.subtotal()
            for item in self._items.values()
        )
    
    def __repr__(self) -> str:
        return (
            f"ShoppingCart(user={self._user.email}, "
            f"items={self.item_count}, total={self.calculate_total()}€)"
        )
    
    def __str__(self) -> str:
        if self.is_empty():
            return "Panier vide"
        
        lines = [f"Panier de {self._user.name}:"]
        for item in self._items.values():
            lines.append(f"  - {item}")
        lines.append(f"Total: {self.calculate_total()}€")
        
        return "\n".join(lines)


# Ce fichier continue avec les modèles Order, OrderState, etc...
# ============================================================================

# ============================================================================
# [SHOPPING_TROLLEY] PROJET PRATIQUE 1 : E-COMMERCE (PARTIE 2)
# ============================================================================
#
# Suite du projet : Commandes, Services, Strategies
#
# [OBJECTIF] CONTENU :
# - Modèle Order avec Pattern State
# - Services métier (OrderService, CartService)
# - Strategy Pattern (Prix, Livraison, Paiement)
# - Observer Pattern (Notifications)
# - Command Pattern (Actions annulables)
#
# ============================================================================


# Suite de la Partie 1...

from enum import Enum
from typing import List, Optional, Dict
from decimal import Decimal
from datetime import datetime
from abc import ABC, abstractmethod
import uuid


"""
═══════════════════════════════════════════════════════════════════════════
MODÈLE 4 : COMMANDE (avec Pattern State)
═══════════════════════════════════════════════════════════════════════════
"""

class OrderStatus(Enum):
    """Statuts possibles d'une commande."""
    PENDING = "pending"          # En attente
    CONFIRMED = "confirmed"      # Confirmée
    PROCESSING = "processing"    # En traitement
    SHIPPED = "shipped"          # Expédiée
    DELIVERED = "delivered"      # Livrée
    CANCELLED = "cancelled"      # Annulée


class Order:
    """
    Commande client.
    
    Représente une commande avec ses items, statut, paiement, livraison.
    Utilise le Pattern State pour gérer les transitions d'état.
    
    Attributes:
        id: Identifiant unique
        user: Client
        items: Liste d'items commandés
        status: Statut actuel
        total: Montant total
        created_at: Date de création
    """
    
    def __init__(
        self,
        user: User,
        items: List[CartItem],
        order_id: Optional[str] = None
    ):
        """
        Initialise commande depuis panier.
        
        Args:
            user: Client
            items: Items du panier
            order_id: ID personnalisé (auto-généré si None)
            
        Raises:
            ValueError: Si aucun item
        """
        if not items:
            raise ValueError("Commande doit contenir au moins un item")
        
        self._id = order_id or f"ORD-{uuid.uuid4().hex[:8].upper()}"
        self._user = user
        self._items = items.copy()  # Copie pour éviter modifications
        self._status = OrderStatus.PENDING
        self._created_at = datetime.now()
        self._confirmed_at: Optional[datetime] = None
        self._shipped_at: Optional[datetime] = None
        self._delivered_at: Optional[datetime] = None
        self._cancelled_at: Optional[datetime] = None
        
        # Calculer total à la création
        self._total = sum(item.subtotal() for item in self._items)
        
        # Pattern State : État actuel de la commande
        self._state: 'OrderState' = PendingState(self)
    
    # Properties
    @property
    def id(self) -> str:
        return self._id
    
    @property
    def user(self) -> User:
        return self._user
    
    @property
    def items(self) -> List[CartItem]:
        return self._items.copy()
    
    @property
    def status(self) -> OrderStatus:
        return self._status
    
    @property
    def total(self) -> Decimal:
        return self._total
    
    @property
    def created_at(self) -> datetime:
        return self._created_at
    
    @property
    def confirmed_at(self) -> Optional[datetime]:
        return self._confirmed_at
    
    @property
    def shipped_at(self) -> Optional[datetime]:
        return self._shipped_at
    
    @property
    def delivered_at(self) -> Optional[datetime]:
        return self._delivered_at
    
    @property
    def cancelled_at(self) -> Optional[datetime]:
        return self._cancelled_at
    
    # Méthodes de transition (délèguent à l'état)
    def confirm(self) -> None:
        """
        Confirmer commande.
        
        Délègue à l'état actuel.
        """
        self._state.confirm()
    
    def process(self) -> None:
        """Traiter commande."""
        self._state.process()
    
    def ship(self) -> None:
        """Expédier commande."""
        self._state.ship()
    
    def deliver(self) -> None:
        """Livrer commande."""
        self._state.deliver()
    
    def cancel(self) -> None:
        """Annuler commande."""
        self._state.cancel()
    
    # Méthodes internes (utilisées par les états)
    def _set_state(self, state: 'OrderState') -> None:
        """Changer état (usage interne)."""
        self._state = state
    
    def _set_status(self, status: OrderStatus) -> None:
        """Changer statut (usage interne)."""
        self._status = status
    
    def _mark_confirmed(self) -> None:
        """Marquer comme confirmé."""
        self._confirmed_at = datetime.now()
    
    def _mark_shipped(self) -> None:
        """Marquer comme expédié."""
        self._shipped_at = datetime.now()
    
    def _mark_delivered(self) -> None:
        """Marquer comme livré."""
        self._delivered_at = datetime.now()
    
    def _mark_cancelled(self) -> None:
        """Marquer comme annulé."""
        self._cancelled_at = datetime.now()
    
    def can_be_modified(self) -> bool:
        """Vérifier si commande peut être modifiée."""
        return self._status == OrderStatus.PENDING
    
    def can_be_cancelled(self) -> bool:
        """Vérifier si commande peut être annulée."""
        return self._status in [
            OrderStatus.PENDING,
            OrderStatus.CONFIRMED
        ]
    
    def __repr__(self) -> str:
        return (
            f"Order(id={self.id!r}, user={self.user.email}, "
            f"status={self.status.value}, total={self.total}€)"
        )
    
    def __str__(self) -> str:
        lines = [
            f"Commande {self.id}",
            f"Client: {self.user.name}",
            f"Statut: {self.status.value}",
            f"Date: {self.created_at.strftime('%Y-%m-%d %H:%M')}",
            "",
            "Items:"
        ]
        
        for item in self._items:
            lines.append(f"  - {item}")
        
        lines.append("")
        lines.append(f"Total: {self.total}€")
        
        return "\n".join(lines)


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN STATE : États de la commande
═══════════════════════════════════════════════════════════════════════════
"""

class OrderState(ABC):
    """
    [IDEE] PATTERN STATE
    
    État abstrait d'une commande.
    Chaque état concret implémente les transitions possibles.
    """
    
    def __init__(self, order: Order):
        """
        Initialise état.
        
        Args:
            order: Commande associée
        """
        self._order = order
    
    @abstractmethod
    def confirm(self) -> None:
        """Confirmer commande."""
        pass
    
    @abstractmethod
    def process(self) -> None:
        """Traiter commande."""
        pass
    
    @abstractmethod
    def ship(self) -> None:
        """Expédier commande."""
        pass
    
    @abstractmethod
    def deliver(self) -> None:
        """Livrer commande."""
        pass
    
    @abstractmethod
    def cancel(self) -> None:
        """Annuler commande."""
        pass
    
    def _invalid_transition(self, action: str) -> None:
        """Lever exception pour transition invalide."""
        raise ValueError(
            f"Cannot {action} order in {self._order.status.value} state"
        )


class PendingState(OrderState):
    """État : En attente."""
    
    def confirm(self) -> None:
        """Confirmer -> passe à Confirmed."""
        self._order._set_status(OrderStatus.CONFIRMED)
        self._order._mark_confirmed()
        self._order._set_state(ConfirmedState(self._order))
    
    def process(self) -> None:
        """Impossible depuis Pending."""
        self._invalid_transition("process")
    
    def ship(self) -> None:
        """Impossible depuis Pending."""
        self._invalid_transition("ship")
    
    def deliver(self) -> None:
        """Impossible depuis Pending."""
        self._invalid_transition("deliver")
    
    def cancel(self) -> None:
        """Annuler -> passe à Cancelled."""
        self._order._set_status(OrderStatus.CANCELLED)
        self._order._mark_cancelled()
        self._order._set_state(CancelledState(self._order))


class ConfirmedState(OrderState):
    """État : Confirmée."""
    
    def confirm(self) -> None:
        """Déjà confirmée."""
        pass  # Idempotent
    
    def process(self) -> None:
        """Traiter -> passe à Processing."""
        self._order._set_status(OrderStatus.PROCESSING)
        self._order._set_state(ProcessingState(self._order))
    
    def ship(self) -> None:
        """Impossible depuis Confirmed."""
        self._invalid_transition("ship")
    
    def deliver(self) -> None:
        """Impossible depuis Confirmed."""
        self._invalid_transition("deliver")
    
    def cancel(self) -> None:
        """Annuler -> passe à Cancelled."""
        self._order._set_status(OrderStatus.CANCELLED)
        self._order._mark_cancelled()
        self._order._set_state(CancelledState(self._order))


class ProcessingState(OrderState):
    """État : En traitement."""
    
    def confirm(self) -> None:
        """Déjà confirmée."""
        pass
    
    def process(self) -> None:
        """Déjà en traitement."""
        pass
    
    def ship(self) -> None:
        """Expédier -> passe à Shipped."""
        self._order._set_status(OrderStatus.SHIPPED)
        self._order._mark_shipped()
        self._order._set_state(ShippedState(self._order))
    
    def deliver(self) -> None:
        """Impossible depuis Processing."""
        self._invalid_transition("deliver")
    
    def cancel(self) -> None:
        """Annulation plus difficile en traitement."""
        # En réalité, pourrait nécessiter approbation
        self._order._set_status(OrderStatus.CANCELLED)
        self._order._mark_cancelled()
        self._order._set_state(CancelledState(self._order))


class ShippedState(OrderState):
    """État : Expédiée."""
    
    def confirm(self) -> None:
        """Déjà confirmée."""
        pass
    
    def process(self) -> None:
        """Déjà traitée."""
        pass
    
    def ship(self) -> None:
        """Déjà expédiée."""
        pass
    
    def deliver(self) -> None:
        """Livrer -> passe à Delivered."""
        self._order._set_status(OrderStatus.DELIVERED)
        self._order._mark_delivered()
        self._order._set_state(DeliveredState(self._order))
    
    def cancel(self) -> None:
        """Impossible d'annuler une commande expédiée."""
        raise ValueError("Cannot cancel shipped order")


class DeliveredState(OrderState):
    """État : Livrée (état final)."""
    
    def confirm(self) -> None:
        """Déjà confirmée."""
        pass
    
    def process(self) -> None:
        """Déjà traitée."""
        pass
    
    def ship(self) -> None:
        """Déjà expédiée."""
        pass
    
    def deliver(self) -> None:
        """Déjà livrée."""
        pass
    
    def cancel(self) -> None:
        """Impossible d'annuler commande livrée."""
        raise ValueError("Cannot cancel delivered order")


class CancelledState(OrderState):
    """État : Annulée (état final)."""
    
    def confirm(self) -> None:
        """Impossible."""
        raise ValueError("Cannot confirm cancelled order")
    
    def process(self) -> None:
        """Impossible."""
        raise ValueError("Cannot process cancelled order")
    
    def ship(self) -> None:
        """Impossible."""
        raise ValueError("Cannot ship cancelled order")
    
    def deliver(self) -> None:
        """Impossible."""
        raise ValueError("Cannot deliver cancelled order")
    
    def cancel(self) -> None:
        """Déjà annulée."""
        pass


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN STRATEGY : Stratégies de prix
═══════════════════════════════════════════════════════════════════════════
"""

class PricingStrategy(ABC):
    """
    [IDEE] PATTERN STRATEGY
    
    Stratégie de calcul de prix.
    Permet différentes politiques de prix interchangeables.
    """
    
    @abstractmethod
    def calculate_price(self, base_price: Decimal, user: User) -> Decimal:
        """
        Calculer prix final.
        
        Args:
            base_price: Prix de base
            user: Utilisateur (pour personnalisation)
            
        Returns:
            Prix final
        """
        pass
    
    @abstractmethod
    def get_description(self) -> str:
        """Description de la stratégie."""
        pass


class RegularPricingStrategy(PricingStrategy):
    """Stratégie : Prix normal (pas de remise)."""
    
    def calculate_price(self, base_price: Decimal, user: User) -> Decimal:
        return base_price
    
    def get_description(self) -> str:
        return "Prix normal"


class PercentageDiscountStrategy(PricingStrategy):
    """Stratégie : Remise en pourcentage."""
    
    def __init__(self, discount_percentage: Decimal):
        """
        Initialise stratégie.
        
        Args:
            discount_percentage: Pourcentage de remise (ex: 10 pour 10%)
        """
        if not 0 <= discount_percentage <= 100:
            raise ValueError("Discount must be between 0 and 100")
        self.discount_percentage = discount_percentage
    
    def calculate_price(self, base_price: Decimal, user: User) -> Decimal:
        discount = base_price * (self.discount_percentage / 100)
        return base_price - discount
    
    def get_description(self) -> str:
        return f"Remise de {self.discount_percentage}%"


class FixedAmountDiscountStrategy(PricingStrategy):
    """Stratégie : Remise montant fixe."""
    
    def __init__(self, discount_amount: Decimal):
        """
        Initialise stratégie.
        
        Args:
            discount_amount: Montant de remise
        """
        if discount_amount < 0:
            raise ValueError("Discount amount must be positive")
        self.discount_amount = discount_amount
    
    def calculate_price(self, base_price: Decimal, user: User) -> Decimal:
        final_price = base_price - self.discount_amount
        return max(final_price, Decimal("0"))  # Pas de prix négatif
    
    def get_description(self) -> str:
        return f"Remise de {self.discount_amount}€"


class LoyaltyPricingStrategy(PricingStrategy):
    """Stratégie : Prix fidélité basé sur ancienneté."""
    
    def calculate_price(self, base_price: Decimal, user: User) -> Decimal:
        # Calculer ancienneté en jours
        days_since_registration = (datetime.now() - user.created_at).days
        
        # Remise progressive : 1% par mois (max 20%)
        months = days_since_registration / 30
        discount_percentage = min(months, 20)
        
        discount = base_price * (Decimal(str(discount_percentage)) / 100)
        return base_price - discount
    
    def get_description(self) -> str:
        return "Prix fidélité (jusqu'à -20%)"


class BulkPricingStrategy(PricingStrategy):
    """Stratégie : Prix dégressif selon quantité."""
    
    def __init__(self, thresholds: Dict[int, Decimal]):
        """
        Initialise stratégie.
        
        Args:
            thresholds: {quantité_min: pourcentage_remise}
                       Ex: {10: 5, 50: 10, 100: 20}
        """
        self.thresholds = dict(sorted(thresholds.items(), reverse=True))
    
    def calculate_price_for_quantity(
        self,
        unit_price: Decimal,
        quantity: int,
        user: User
    ) -> Decimal:
        """
        Calculer prix selon quantité.
        
        Args:
            unit_price: Prix unitaire
            quantity: Quantité achetée
            user: Utilisateur
            
        Returns:
            Prix final
        """
        # Trouver remise applicable
        discount_percentage = Decimal("0")
        for threshold, discount in self.thresholds.items():
            if quantity >= threshold:
                discount_percentage = discount
                break
        
        base_price = unit_price * quantity
        discount = base_price * (discount_percentage / 100)
        return base_price - discount
    
    def calculate_price(self, base_price: Decimal, user: User) -> Decimal:
        # Pour interface générique (quantité = 1)
        return base_price
    
    def get_description(self) -> str:
        desc = "Prix dégressif : "
        desc += ", ".join(
            f"{qty}+ -> -{disc}%"
            for qty, disc in sorted(self.thresholds.items())
        )
        return desc


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN STRATEGY : Stratégies de livraison
═══════════════════════════════════════════════════════════════════════════
"""

class ShippingStrategy(ABC):
    """Stratégie de livraison."""
    
    @abstractmethod
    def calculate_cost(self, order: Order) -> Decimal:
        """Calculer coût livraison."""
        pass
    
    @abstractmethod
    def estimate_delivery_days(self) -> tuple[int, int]:
        """Estimer délai livraison (min, max jours)."""
        pass
    
    @abstractmethod
    def get_name(self) -> str:
        """Nom de la méthode."""
        pass


class StandardShippingStrategy(ShippingStrategy):
    """Livraison standard."""
    
    def calculate_cost(self, order: Order) -> Decimal:
        # Gratuit si commande > 50€
        if order.total >= Decimal("50"):
            return Decimal("0")
        return Decimal("4.99")
    
    def estimate_delivery_days(self) -> tuple[int, int]:
        return (5, 7)
    
    def get_name(self) -> str:
        return "Livraison standard"


class ExpressShippingStrategy(ShippingStrategy):
    """Livraison express."""
    
    def calculate_cost(self, order: Order) -> Decimal:
        return Decimal("9.99")
    
    def estimate_delivery_days(self) -> tuple[int, int]:
        return (1, 2)
    
    def get_name(self) -> str:
        return "Livraison express"


class NextDayShippingStrategy(ShippingStrategy):
    """Livraison lendemain."""
    
    def calculate_cost(self, order: Order) -> Decimal:
        return Decimal("19.99")
    
    def estimate_delivery_days(self) -> tuple[int, int]:
        return (1, 1)
    
    def get_name(self) -> str:
        return "Livraison lendemain"


class PickupShippingStrategy(ShippingStrategy):
    """Retrait en magasin."""
    
    def calculate_cost(self, order: Order) -> Decimal:
        return Decimal("0")  # Gratuit
    
    def estimate_delivery_days(self) -> tuple[int, int]:
        return (1, 3)  # Disponible en 1-3 jours
    
    def get_name(self) -> str:
        return "Retrait en magasin"


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN OBSERVER : Système de notifications
═══════════════════════════════════════════════════════════════════════════
"""

class OrderObserver(ABC):
    """
    [IDEE] PATTERN OBSERVER
    
    Observateur d'événements de commande.
    """
    
    @abstractmethod
    def on_order_created(self, order: Order) -> None:
        """Commande créée."""
        pass
    
    @abstractmethod
    def on_order_confirmed(self, order: Order) -> None:
        """Commande confirmée."""
        pass
    
    @abstractmethod
    def on_order_shipped(self, order: Order) -> None:
        """Commande expédiée."""
        pass
    
    @abstractmethod
    def on_order_delivered(self, order: Order) -> None:
        """Commande livrée."""
        pass
    
    @abstractmethod
    def on_order_cancelled(self, order: Order) -> None:
        """Commande annulée."""
        pass


class EmailNotificationObserver(OrderObserver):
    """Observateur : Notifications email."""
    
    def on_order_created(self, order: Order) -> None:
        print(f"[EMAIL] Email à {order.user.email}: Commande {order.id} créée")
    
    def on_order_confirmed(self, order: Order) -> None:
        print(f"[EMAIL] Email à {order.user.email}: Commande {order.id} confirmée")
    
    def on_order_shipped(self, order: Order) -> None:
        print(f"[EMAIL] Email à {order.user.email}: Commande {order.id} expédiée")
    
    def on_order_delivered(self, order: Order) -> None:
        print(f"[EMAIL] Email à {order.user.email}: Commande {order.id} livrée")
    
    def on_order_cancelled(self, order: Order) -> None:
        print(f"[EMAIL] Email à {order.user.email}: Commande {order.id} annulée")


class SMSNotificationObserver(OrderObserver):
    """Observateur : Notifications SMS."""
    
    def on_order_created(self, order: Order) -> None:
        print(f"[MOBILE] SMS: Commande {order.id} créée")
    
    def on_order_confirmed(self, order: Order) -> None:
        print(f"[MOBILE] SMS: Commande {order.id} confirmée")
    
    def on_order_shipped(self, order: Order) -> None:
        print(f"[MOBILE] SMS: Commande {order.id} expédiée")
    
    def on_order_delivered(self, order: Order) -> None:
        print(f"[MOBILE] SMS: Commande {order.id} livrée")
    
    def on_order_cancelled(self, order: Order) -> None:
        print(f"[MOBILE] SMS: Commande {order.id} annulée")


class AnalyticsObserver(OrderObserver):
    """Observateur : Analytics."""
    
    def on_order_created(self, order: Order) -> None:
        print(f"[GRAPHIQUE] Analytics: Nouvelle commande {order.total}€")
    
    def on_order_confirmed(self, order: Order) -> None:
        print(f"[GRAPHIQUE] Analytics: Commande confirmée {order.total}€")
    
    def on_order_shipped(self, order: Order) -> None:
        print(f"[GRAPHIQUE] Analytics: Commande expédiée")
    
    def on_order_delivered(self, order: Order) -> None:
        print(f"[GRAPHIQUE] Analytics: Commande livrée avec succès")
    
    def on_order_cancelled(self, order: Order) -> None:
        print(f"[GRAPHIQUE] Analytics: Commande annulée (perte {order.total}€)")


# Ce fichier continue avec les Services...
# ============================================================================

# ============================================================================
# [SHOPPING_TROLLEY] PROJET PRATIQUE 1 : E-COMMERCE (PARTIE 3)
# ============================================================================
#
# Suite du projet : Services, Repositories, Command, Facade
#
# [OBJECTIF] CONTENU :
# - Pattern Repository (abstraction persistance)
# - Services métier (OrderService, CartService, PaymentService)
# - Pattern Command (actions annulables)
# - Pattern Facade (simplification checkout)
# - Pattern Singleton (Configuration)
#
# ============================================================================


# Suite des Parties 1 et 2...

from typing import List, Optional, Dict, Callable
from abc import ABC, abstractmethod
from decimal import Decimal
from datetime import datetime
import uuid


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN REPOSITORY : Abstraction de la persistance
═══════════════════════════════════════════════════════════════════════════

[IDEE] REPOSITORY PATTERN

Avantages :
- Sépare domaine de la persistance (DIP)
- Testable facilement (mocks)
- Changement de DB transparent
- Interface métier claire
"""

class Repository(ABC):
    """
    [IDEE] PATTERN REPOSITORY
    
    Interface générique pour persistance.
    Principe DIP : dépendre d'abstraction, pas d'implémentation.
    """
    
    @abstractmethod
    def save(self, entity) -> None:
        """Sauvegarder entité."""
        pass
    
    @abstractmethod
    def find_by_id(self, entity_id: str):
        """Trouver par ID."""
        pass
    
    @abstractmethod
    def find_all(self) -> List:
        """Trouver toutes les entités."""
        pass
    
    @abstractmethod
    def delete(self, entity_id: str) -> None:
        """Supprimer entité."""
        pass


class ProductRepository(Repository):
    """Repository pour produits."""
    
    @abstractmethod
    def find_by_category(self, category: ProductCategory) -> List[Product]:
        """Trouver produits par catégorie."""
        pass
    
    @abstractmethod
    def find_available(self) -> List[Product]:
        """Trouver produits disponibles (stock > 0)."""
        pass
    
    @abstractmethod
    def search(self, query: str) -> List[Product]:
        """Rechercher produits."""
        pass


class InMemoryProductRepository(ProductRepository):
    """
    Implémentation en mémoire (pour développement/tests).
    
    En production, utiliser SQLProductRepository, MongoProductRepository, etc.
    """
    
    def __init__(self):
        """Initialise repository vide."""
        self._products: Dict[str, Product] = {}
    
    def save(self, product: Product) -> None:
        """
        Sauvegarder produit.
        
        Args:
            product: Produit à sauvegarder
        """
        self._products[product.id] = product
    
    def find_by_id(self, product_id: str) -> Optional[Product]:
        """
        Trouver produit par ID.
        
        Args:
            product_id: ID du produit
            
        Returns:
            Produit si trouvé, None sinon
        """
        return self._products.get(product_id)
    
    def find_all(self) -> List[Product]:
        """Retourner tous les produits."""
        return list(self._products.values())
    
    def delete(self, product_id: str) -> None:
        """
        Supprimer produit.
        
        Args:
            product_id: ID du produit
            
        Raises:
            KeyError: Si produit introuvable
        """
        if product_id not in self._products:
            raise KeyError(f"Product {product_id} not found")
        del self._products[product_id]
    
    def find_by_category(self, category: ProductCategory) -> List[Product]:
        """Trouver produits d'une catégorie."""
        return [
            p for p in self._products.values()
            if p.category == category
        ]
    
    def find_available(self) -> List[Product]:
        """Trouver produits disponibles."""
        return [
            p for p in self._products.values()
            if p.is_available()
        ]
    
    def search(self, query: str) -> List[Product]:
        """
        Rechercher produits par nom ou description.
        
        Args:
            query: Terme de recherche
            
        Returns:
            Liste de produits correspondants
        """
        query_lower = query.lower()
        return [
            p for p in self._products.values()
            if query_lower in p.name.lower() 
            or query_lower in p.description.lower()
        ]


class UserRepository(Repository):
    """Repository pour utilisateurs."""
    
    @abstractmethod
    def find_by_email(self, email: str) -> Optional[User]:
        """Trouver utilisateur par email."""
        pass


class InMemoryUserRepository(UserRepository):
    """Implémentation en mémoire pour utilisateurs."""
    
    def __init__(self):
        self._users: Dict[str, User] = {}
        self._email_index: Dict[str, str] = {}  # email -> user_id
    
    def save(self, user: User) -> None:
        """Sauvegarder utilisateur."""
        self._users[user.id] = user
        self._email_index[user.email] = user.id
    
    def find_by_id(self, user_id: str) -> Optional[User]:
        """Trouver utilisateur par ID."""
        return self._users.get(user_id)
    
    def find_all(self) -> List[User]:
        """Retourner tous les utilisateurs."""
        return list(self._users.values())
    
    def delete(self, user_id: str) -> None:
        """Supprimer utilisateur."""
        if user_id not in self._users:
            raise KeyError(f"User {user_id} not found")
        
        user = self._users[user_id]
        del self._email_index[user.email]
        del self._users[user_id]
    
    def find_by_email(self, email: str) -> Optional[User]:
        """
        Trouver utilisateur par email.
        
        Args:
            email: Email de l'utilisateur
            
        Returns:
            Utilisateur si trouvé, None sinon
        """
        user_id = self._email_index.get(email.lower())
        if user_id:
            return self._users.get(user_id)
        return None


class OrderRepository(Repository):
    """Repository pour commandes."""
    
    @abstractmethod
    def find_by_user(self, user_id: str) -> List[Order]:
        """Trouver commandes d'un utilisateur."""
        pass
    
    @abstractmethod
    def find_by_status(self, status: OrderStatus) -> List[Order]:
        """Trouver commandes par statut."""
        pass


class InMemoryOrderRepository(OrderRepository):
    """Implémentation en mémoire pour commandes."""
    
    def __init__(self):
        self._orders: Dict[str, Order] = {}
        self._user_index: Dict[str, List[str]] = {}  # user_id -> [order_ids]
    
    def save(self, order: Order) -> None:
        """Sauvegarder commande."""
        self._orders[order.id] = order
        
        # Indexer par utilisateur
        if order.user.id not in self._user_index:
            self._user_index[order.user.id] = []
        
        if order.id not in self._user_index[order.user.id]:
            self._user_index[order.user.id].append(order.id)
    
    def find_by_id(self, order_id: str) -> Optional[Order]:
        """Trouver commande par ID."""
        return self._orders.get(order_id)
    
    def find_all(self) -> List[Order]:
        """Retourner toutes les commandes."""
        return list(self._orders.values())
    
    def delete(self, order_id: str) -> None:
        """Supprimer commande."""
        if order_id not in self._orders:
            raise KeyError(f"Order {order_id} not found")
        
        order = self._orders[order_id]
        
        # Retirer de l'index
        if order.user.id in self._user_index:
            self._user_index[order.user.id].remove(order_id)
        
        del self._orders[order_id]
    
    def find_by_user(self, user_id: str) -> List[Order]:
        """Trouver commandes d'un utilisateur."""
        order_ids = self._user_index.get(user_id, [])
        return [self._orders[oid] for oid in order_ids if oid in self._orders]
    
    def find_by_status(self, status: OrderStatus) -> List[Order]:
        """Trouver commandes par statut."""
        return [
            order for order in self._orders.values()
            if order.status == status
        ]


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN SINGLETON : Configuration globale
═══════════════════════════════════════════════════════════════════════════
"""

class AppConfig:
    """
    [IDEE] PATTERN SINGLETON
    
    Configuration globale de l'application.
    Une seule instance dans toute l'application.
    """
    
    _instance: Optional['AppConfig'] = None
    _initialized: bool = False
    
    def __new__(cls):
        """Créer ou retourner instance unique."""
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self):
        """Initialiser configuration (une seule fois)."""
        if not AppConfig._initialized:
            # Configuration par défaut
            self.tax_rate = Decimal("0.20")  # 20% TVA
            self.free_shipping_threshold = Decimal("50.00")
            self.min_order_amount = Decimal("5.00")
            self.max_cart_items = 100
            self.currency = "EUR"
            self.currency_symbol = "€"
            
            # Notifications
            self.email_notifications_enabled = True
            self.sms_notifications_enabled = False
            
            # Paiement
            self.payment_gateway = "stripe"
            
            AppConfig._initialized = True
    
    @classmethod
    def reset(cls):
        """Réinitialiser singleton (pour tests)."""
        cls._instance = None
        cls._initialized = False
    
    def __repr__(self):
        return f"AppConfig(tax_rate={self.tax_rate}, currency={self.currency})"


"""
═══════════════════════════════════════════════════════════════════════════
SERVICES MÉTIER
═══════════════════════════════════════════════════════════════════════════

[IDEE] SERVICES

Services = Logique métier orchestrant plusieurs entités.
Suivent principe SRP (Single Responsibility).
"""

class CartService:
    """
    Service de gestion du panier.
    
    Coordonne opérations sur le panier avec validation métier.
    """
    
    def __init__(self, product_repository: ProductRepository):
        """
        Initialise service.
        
        Args:
            product_repository: Repository produits (DIP)
        """
        self.product_repo = product_repository
        self.config = AppConfig()
    
    def add_product_to_cart(
        self,
        cart: ShoppingCart,
        product_id: str,
        quantity: int = 1
    ) -> None:
        """
        Ajouter produit au panier.
        
        Args:
            cart: Panier
            product_id: ID du produit
            quantity: Quantité à ajouter
            
        Raises:
            ValueError: Si produit introuvable ou stock insuffisant
        """
        # Trouver produit
        product = self.product_repo.find_by_id(product_id)
        if not product:
            raise ValueError(f"Product {product_id} not found")
        
        # Vérifier limite panier
        if cart.item_count >= self.config.max_cart_items:
            raise ValueError(
                f"Cart is full (max {self.config.max_cart_items} items)"
            )
        
        # Ajouter au panier (validation stock faite dans ShoppingCart)
        cart.add_item(product, quantity)
    
    def remove_product_from_cart(
        self,
        cart: ShoppingCart,
        product_id: str
    ) -> None:
        """
        Retirer produit du panier.
        
        Args:
            cart: Panier
            product_id: ID du produit
        """
        cart.remove_item(product_id)
    
    def update_quantity(
        self,
        cart: ShoppingCart,
        product_id: str,
        quantity: int
    ) -> None:
        """
        Modifier quantité d'un produit.
        
        Args:
            cart: Panier
            product_id: ID du produit
            quantity: Nouvelle quantité
        """
        cart.update_quantity(product_id, quantity)
    
    def clear_cart(self, cart: ShoppingCart) -> None:
        """Vider panier complètement."""
        cart.clear()
    
    def apply_pricing_strategy(
        self,
        cart: ShoppingCart,
        strategy: PricingStrategy
    ) -> Decimal:
        """
        Calculer total avec stratégie de prix.
        
        Args:
            cart: Panier
            strategy: Stratégie de prix
            
        Returns:
            Total avec stratégie appliquée
        """
        base_total = cart.calculate_total()
        return strategy.calculate_price(base_total, cart.user)
    
    def validate_cart(self, cart: ShoppingCart) -> tuple[bool, List[str]]:
        """
        Valider panier avant commande.
        
        Args:
            cart: Panier à valider
            
        Returns:
            (est_valide, liste_erreurs)
        """
        errors = []
        
        # Panier vide
        if cart.is_empty():
            errors.append("Cart is empty")
        
        # Vérifier montant minimum
        if cart.calculate_total() < self.config.min_order_amount:
            errors.append(
                f"Minimum order amount is {self.config.min_order_amount}€"
            )
        
        # Vérifier stock pour chaque item
        for item in cart.items:
            if not item.product.has_sufficient_stock(item.quantity):
                errors.append(
                    f"Insufficient stock for {item.product.name}. "
                    f"Available: {item.product.stock}"
                )
        
        return (len(errors) == 0, errors)


class OrderService:
    """
    Service de gestion des commandes.
    
    Coordonne création, modification, suivi des commandes.
    Applique Pattern Observer pour notifications.
    """
    
    def __init__(
        self,
        order_repository: OrderRepository,
        product_repository: ProductRepository
    ):
        """
        Initialise service.
        
        Args:
            order_repository: Repository commandes
            product_repository: Repository produits
        """
        self.order_repo = order_repository
        self.product_repo = product_repository
        self.observers: List[OrderObserver] = []
        self.config = AppConfig()
    
    def add_observer(self, observer: OrderObserver) -> None:
        """
        Ajouter observateur (Pattern Observer).
        
        Args:
            observer: Observateur à ajouter
        """
        if observer not in self.observers:
            self.observers.append(observer)
    
    def remove_observer(self, observer: OrderObserver) -> None:
        """Retirer observateur."""
        if observer in self.observers:
            self.observers.remove(observer)
    
    def _notify_created(self, order: Order) -> None:
        """Notifier observateurs : commande créée."""
        for observer in self.observers:
            observer.on_order_created(order)
    
    def _notify_confirmed(self, order: Order) -> None:
        """Notifier observateurs : commande confirmée."""
        for observer in self.observers:
            observer.on_order_confirmed(order)
    
    def _notify_shipped(self, order: Order) -> None:
        """Notifier observateurs : commande expédiée."""
        for observer in self.observers:
            observer.on_order_shipped(order)
    
    def _notify_delivered(self, order: Order) -> None:
        """Notifier observateurs : commande livrée."""
        for observer in self.observers:
            observer.on_order_delivered(order)
    
    def _notify_cancelled(self, order: Order) -> None:
        """Notifier observateurs : commande annulée."""
        for observer in self.observers:
            observer.on_order_cancelled(order)
    
    def create_order_from_cart(self, cart: ShoppingCart) -> Order:
        """
        Créer commande depuis panier.
        
        Args:
            cart: Panier
            
        Returns:
            Commande créée
            
        Raises:
            ValueError: Si panier invalide
        """
        # Valider panier
        is_valid, errors = CartService(self.product_repo).validate_cart(cart)
        if not is_valid:
            raise ValueError(f"Invalid cart: {', '.join(errors)}")
        
        # Créer commande
        order = Order(cart.user, cart.items)
        
        # Sauvegarder
        self.order_repo.save(order)
        
        # Notifier observateurs
        self._notify_created(order)
        
        return order
    
    def confirm_order(self, order_id: str) -> Order:
        """
        Confirmer commande.
        
        Args:
            order_id: ID de la commande
            
        Returns:
            Commande confirmée
            
        Raises:
            ValueError: Si commande introuvable
        """
        order = self.order_repo.find_by_id(order_id)
        if not order:
            raise ValueError(f"Order {order_id} not found")
        
        # Confirmer (Pattern State gère transitions)
        order.confirm()
        
        # Décrémenter stock
        for item in order.items:
            item.product.decrease_stock(item.quantity)
            self.product_repo.save(item.product)
        
        # Sauvegarder
        self.order_repo.save(order)
        
        # Notifier
        self._notify_confirmed(order)
        
        return order
    
    def ship_order(self, order_id: str) -> Order:
        """
        Expédier commande.
        
        Args:
            order_id: ID de la commande
            
        Returns:
            Commande expédiée
        """
        order = self.order_repo.find_by_id(order_id)
        if not order:
            raise ValueError(f"Order {order_id} not found")
        
        # Passer en processing puis ship
        if order.status == OrderStatus.CONFIRMED:
            order.process()
        
        order.ship()
        
        # Sauvegarder
        self.order_repo.save(order)
        
        # Notifier
        self._notify_shipped(order)
        
        return order
    
    def deliver_order(self, order_id: str) -> Order:
        """Livrer commande."""
        order = self.order_repo.find_by_id(order_id)
        if not order:
            raise ValueError(f"Order {order_id} not found")
        
        order.deliver()
        
        self.order_repo.save(order)
        self._notify_delivered(order)
        
        return order
    
    def cancel_order(self, order_id: str) -> Order:
        """
        Annuler commande.
        
        Args:
            order_id: ID de la commande
            
        Returns:
            Commande annulée
        """
        order = self.order_repo.find_by_id(order_id)
        if not order:
            raise ValueError(f"Order {order_id} not found")
        
        # Annuler
        order.cancel()
        
        # Restaurer stock si commande était confirmée
        if order.confirmed_at is not None:
            for item in order.items:
                item.product.increase_stock(item.quantity)
                self.product_repo.save(item.product)
        
        # Sauvegarder
        self.order_repo.save(order)
        
        # Notifier
        self._notify_cancelled(order)
        
        return order
    
    def get_user_orders(self, user_id: str) -> List[Order]:
        """
        Récupérer commandes d'un utilisateur.
        
        Args:
            user_id: ID de l'utilisateur
            
        Returns:
            Liste des commandes
        """
        return self.order_repo.find_by_user(user_id)
    
    def get_order(self, order_id: str) -> Optional[Order]:
        """Récupérer une commande."""
        return self.order_repo.find_by_id(order_id)


class PaymentService:
    """
    Service de paiement.
    
    Gère traitement des paiements avec différentes passerelles.
    """
    
    def __init__(self):
        """Initialise service."""
        self.config = AppConfig()
    
    def process_payment(
        self,
        order: Order,
        payment_method: str,
        payment_details: Dict
    ) -> bool:
        """
        Traiter paiement.
        
        Args:
            order: Commande à payer
            payment_method: Méthode (card, paypal, etc.)
            payment_details: Détails paiement
            
        Returns:
            True si paiement réussi
            
        Note:
            En réel, utiliserait Pattern Adapter pour APIs externes
        """
        print(f"[CARTE] Processing payment for order {order.id}")
        print(f"   Amount: {order.total}€")
        print(f"   Method: {payment_method}")
        
        # Simuler traitement
        # En réel : appel API Stripe, PayPal, etc.
        
        return True


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN COMMAND : Actions annulables
═══════════════════════════════════════════════════════════════════════════
"""

class Command(ABC):
    """
    [IDEE] PATTERN COMMAND
    
    Commande abstraite (action annulable).
    Permet undo/redo, historique, transactions.
    """
    
    @abstractmethod
    def execute(self) -> None:
        """Exécuter commande."""
        pass
    
    @abstractmethod
    def undo(self) -> None:
        """Annuler commande."""
        pass
    
    @abstractmethod
    def get_description(self) -> str:
        """Description de la commande."""
        pass


class AddToCartCommand(Command):
    """Commande : Ajouter produit au panier."""
    
    def __init__(
        self,
        cart: ShoppingCart,
        product: Product,
        quantity: int
    ):
        """
        Initialise commande.
        
        Args:
            cart: Panier
            product: Produit à ajouter
            quantity: Quantité
        """
        self.cart = cart
        self.product = product
        self.quantity = quantity
        self._executed = False
    
    def execute(self) -> None:
        """Ajouter produit."""
        self.cart.add_item(self.product, self.quantity)
        self._executed = True
        print(f"[OK] Added {self.product.name} x{self.quantity} to cart")
    
    def undo(self) -> None:
        """Retirer produit."""
        if not self._executed:
            raise ValueError("Cannot undo command that hasn't been executed")
        
        self.cart.remove_item(self.product.id)
        self._executed = False
        print(f"<-  Removed {self.product.name} from cart")
    
    def get_description(self) -> str:
        return f"Add {self.product.name} x{self.quantity}"


class RemoveFromCartCommand(Command):
    """Commande : Retirer produit du panier."""
    
    def __init__(self, cart: ShoppingCart, product_id: str):
        self.cart = cart
        self.product_id = product_id
        self._removed_item: Optional[CartItem] = None
    
    def execute(self) -> None:
        """Retirer produit."""
        # Sauvegarder item pour undo
        for item in self.cart.items:
            if item.product.id == self.product_id:
                self._removed_item = CartItem(item.product, item.quantity)
                break
        
        if not self._removed_item:
            raise ValueError(f"Product {self.product_id} not in cart")
        
        self.cart.remove_item(self.product_id)
        print(f"[OK] Removed {self._removed_item.product.name} from cart")
    
    def undo(self) -> None:
        """Rajouter produit."""
        if not self._removed_item:
            raise ValueError("Cannot undo: no item was removed")
        
        self.cart.add_item(
            self._removed_item.product,
            self._removed_item.quantity
        )
        print(f"<-  Restored {self._removed_item.product.name} to cart")
    
    def get_description(self) -> str:
        if self._removed_item:
            return f"Remove {self._removed_item.product.name}"
        return f"Remove product {self.product_id}"


class UpdateQuantityCommand(Command):
    """Commande : Modifier quantité."""
    
    def __init__(
        self,
        cart: ShoppingCart,
        product_id: str,
        new_quantity: int
    ):
        self.cart = cart
        self.product_id = product_id
        self.new_quantity = new_quantity
        self._old_quantity: Optional[int] = None
    
    def execute(self) -> None:
        """Modifier quantité."""
        # Sauvegarder ancienne quantité pour undo
        for item in self.cart.items:
            if item.product.id == self.product_id:
                self._old_quantity = item.quantity
                break
        
        if self._old_quantity is None:
            raise ValueError(f"Product {self.product_id} not in cart")
        
        self.cart.update_quantity(self.product_id, self.new_quantity)
        print(f"[OK] Updated quantity: {self._old_quantity} -> {self.new_quantity}")
    
    def undo(self) -> None:
        """Restaurer ancienne quantité."""
        if self._old_quantity is None:
            raise ValueError("Cannot undo: old quantity unknown")
        
        self.cart.update_quantity(self.product_id, self._old_quantity)
        print(f"<-  Restored quantity: {self.new_quantity} -> {self._old_quantity}")
    
    def get_description(self) -> str:
        return f"Update quantity to {self.new_quantity}"


class CommandInvoker:
    """
    [IDEE] INVOKER (Pattern Command)
    
    Exécute commandes et gère historique (undo/redo).
    """
    
    def __init__(self):
        """Initialise invoker."""
        self._history: List[Command] = []
        self._undo_stack: List[Command] = []
    
    def execute(self, command: Command) -> None:
        """
        Exécuter commande.
        
        Args:
            command: Commande à exécuter
        """
        command.execute()
        self._history.append(command)
        
        # Clear redo stack when new command executed
        self._undo_stack.clear()
    
    def undo(self) -> None:
        """
        Annuler dernière commande.
        
        Raises:
            ValueError: Si rien à annuler
        """
        if not self._history:
            raise ValueError("Nothing to undo")
        
        command = self._history.pop()
        command.undo()
        self._undo_stack.append(command)
    
    def redo(self) -> None:
        """
        Refaire commande annulée.
        
        Raises:
            ValueError: Si rien à refaire
        """
        if not self._undo_stack:
            raise ValueError("Nothing to redo")
        
        command = self._undo_stack.pop()
        command.execute()
        self._history.append(command)
    
    def get_history(self) -> List[str]:
        """Obtenir historique des commandes."""
        return [cmd.get_description() for cmd in self._history]
    
    def clear_history(self) -> None:
        """Effacer historique."""
        self._history.clear()
        self._undo_stack.clear()


# Ce fichier continue avec Pattern Facade...
# ============================================================================

# ============================================================================
# [SHOPPING_TROLLEY] PROJET PRATIQUE 1 : E-COMMERCE (PARTIE 4 - FINALE)
# ============================================================================
#
# Fin du projet : Facade, Factory, Tests, Application complète
#
# [OBJECTIF] CONTENU :
# - Pattern Facade (simplification checkout)
# - Pattern Factory (création objets)
# - Tests complets (unittest + pytest)
# - Application finale fonctionnelle
# - Démonstration complète
#
# ============================================================================


# Suite de la Partie 3...

from typing import List, Optional, Dict, Tuple
from decimal import Decimal
from abc import ABC, abstractmethod
import unittest
from unittest.mock import Mock, MagicMock
import pytest


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN FACADE : Simplification du processus de checkout
═══════════════════════════════════════════════════════════════════════════
"""

class CheckoutFacade:
    """
    [IDEE] PATTERN FACADE
    
    Simplifie processus complexe de checkout.
    Interface simple cachant orchestration complexe de services.
    
    Processus checkout :
    1. Valider panier
    2. Appliquer stratégie de prix
    3. Calculer frais de livraison
    4. Créer commande
    5. Traiter paiement
    6. Confirmer commande
    7. Notifier client
    """
    
    def __init__(
        self,
        cart_service: CartService,
        order_service: OrderService,
        payment_service: PaymentService,
        product_repo: ProductRepository
    ):
        """
        Initialise facade.
        
        Args:
            cart_service: Service panier
            order_service: Service commandes
            payment_service: Service paiement
            product_repo: Repository produits
        """
        self.cart_service = cart_service
        self.order_service = order_service
        self.payment_service = payment_service
        self.product_repo = product_repo
        self.config = AppConfig()
    
    def checkout(
        self,
        cart: ShoppingCart,
        pricing_strategy: Optional[PricingStrategy] = None,
        shipping_strategy: Optional[ShippingStrategy] = None,
        payment_method: str = "card",
        payment_details: Optional[Dict] = None
    ) -> Tuple[Order, Decimal]:
        """
        [IDEE] MÉTHODE FACADE
        
        Processus complet de checkout en UNE méthode simple.
        Cache toute la complexité.
        
        Args:
            cart: Panier à commander
            pricing_strategy: Stratégie de prix (None = prix normal)
            shipping_strategy: Stratégie livraison (None = standard)
            payment_method: Méthode de paiement
            payment_details: Détails paiement
            
        Returns:
            (commande, montant_total)
            
        Raises:
            ValueError: Si checkout échoue
        """
        print("\n" + "="*60)
        print("[SHOPPING_TROLLEY] CHECKOUT PROCESS")
        print("="*60)
        
        # 1. Valider panier
        print("\n1⃣  Validating cart...")
        is_valid, errors = self.cart_service.validate_cart(cart)
        if not is_valid:
            raise ValueError(f"Cart validation failed: {', '.join(errors)}")
        print("   [OK] Cart is valid")
        
        # 2. Appliquer stratégie de prix
        print("\n2⃣  Calculating prices...")
        if pricing_strategy is None:
            pricing_strategy = RegularPricingStrategy()
        
        subtotal = cart.calculate_total()
        discounted_total = self.cart_service.apply_pricing_strategy(
            cart, pricing_strategy
        )
        discount = subtotal - discounted_total
        
        print(f"   Subtotal: {subtotal}€")
        print(f"   Discount: -{discount}€ ({pricing_strategy.get_description()})")
        print(f"   After discount: {discounted_total}€")
        
        # 3. Calculer frais de livraison
        print("\n3⃣  Calculating shipping...")
        if shipping_strategy is None:
            shipping_strategy = StandardShippingStrategy()
        
        # Créer commande temporaire pour calcul livraison
        temp_order = Order(cart.user, cart.items)
        shipping_cost = shipping_strategy.calculate_cost(temp_order)
        delivery_days = shipping_strategy.estimate_delivery_days()
        
        print(f"   Shipping: {shipping_cost}€ ({shipping_strategy.get_name()})")
        print(f"   Estimated delivery: {delivery_days[0]}-{delivery_days[1]} days")
        
        # 4. Calculer total final
        total_amount = discounted_total + shipping_cost
        print(f"\n[ARGENT] TOTAL: {total_amount}€")
        
        # 5. Créer commande
        print("\n4⃣  Creating order...")
        order = self.order_service.create_order_from_cart(cart)
        print(f"   [OK] Order created: {order.id}")
        
        # 6. Traiter paiement
        print("\n5⃣  Processing payment...")
        payment_success = self.payment_service.process_payment(
            order,
            payment_method,
            payment_details or {}
        )
        
        if not payment_success:
            # Annuler commande si paiement échoue
            self.order_service.cancel_order(order.id)
            raise ValueError("Payment failed")
        
        print("   [OK] Payment successful")
        
        # 7. Confirmer commande
        print("\n6⃣  Confirming order...")
        order = self.order_service.confirm_order(order.id)
        print("   [OK] Order confirmed")
        
        # 8. Vider panier
        print("\n7⃣  Clearing cart...")
        self.cart_service.clear_cart(cart)
        print("   [OK] Cart cleared")
        
        print("\n" + "="*60)
        print("[OK] CHECKOUT COMPLETE!")
        print("="*60 + "\n")
        
        return order, total_amount
    
    def quick_checkout(
        self,
        cart: ShoppingCart,
        payment_details: Optional[Dict] = None
    ) -> Order:
        """
        Checkout rapide avec paramètres par défaut.
        
        Args:
            cart: Panier
            payment_details: Détails paiement
            
        Returns:
            Commande créée
        """
        order, _ = self.checkout(
            cart=cart,
            pricing_strategy=RegularPricingStrategy(),
            shipping_strategy=StandardShippingStrategy(),
            payment_method="card",
            payment_details=payment_details
        )
        return order


"""
═══════════════════════════════════════════════════════════════════════════
PATTERN FACTORY : Création d'objets
═══════════════════════════════════════════════════════════════════════════
"""

class ProductFactory:
    """
    [IDEE] PATTERN FACTORY
    
    Crée produits selon catégorie avec configuration appropriée.
    """
    
    @staticmethod
    def create_product(
        category: ProductCategory,
        name: str,
        price: Decimal,
        **kwargs
    ) -> Product:
        """
        Créer produit avec configuration par défaut selon catégorie.
        
        Args:
            category: Catégorie
            name: Nom
            price: Prix
            **kwargs: Paramètres additionnels
            
        Returns:
            Produit configuré
        """
        # Configuration par défaut selon catégorie
        defaults = {
            ProductCategory.ELECTRONICS: {
                'stock': 10,
                'description': 'Electronic device'
            },
            ProductCategory.CLOTHING: {
                'stock': 50,
                'description': 'Fashion item'
            },
            ProductCategory.BOOKS: {
                'stock': 100,
                'description': 'Book'
            },
            ProductCategory.HOME: {
                'stock': 30,
                'description': 'Home & Garden'
            },
            ProductCategory.FOOD: {
                'stock': 200,
                'description': 'Food product'
            }
        }
        
        # Fusionner defaults avec kwargs
        config = defaults.get(category, {})
        config.update(kwargs)
        
        return Product(
            name=name,
            price=price,
            category=category,
            **config
        )


class NotificationFactory:
    """
    [IDEE] ABSTRACT FACTORY
    
    Crée ensemble de notifications selon canal.
    """
    
    @staticmethod
    def create_observers(channels: List[str]) -> List[OrderObserver]:
        """
        Créer observateurs de notification.
        
        Args:
            channels: Liste de canaux ('email', 'sms', 'analytics')
            
        Returns:
            Liste d'observateurs
        """
        observers = []
        
        if 'email' in channels:
            observers.append(EmailNotificationObserver())
        
        if 'sms' in channels:
            observers.append(SMSNotificationObserver())
        
        if 'analytics' in channels:
            observers.append(AnalyticsObserver())
        
        return observers


"""
═══════════════════════════════════════════════════════════════════════════
TESTS COMPLETS
═══════════════════════════════════════════════════════════════════════════
"""

class TestProduct(unittest.TestCase):
    """Tests unitaires pour Product."""
    
    def test_create_product(self):
        """Test création produit valide."""
        product = Product(
            name="Laptop",
            price=Decimal("999.99"),
            category=ProductCategory.ELECTRONICS,
            stock=10
        )
        
        self.assertEqual(product.name, "Laptop")
        self.assertEqual(product.price, Decimal("999.99"))
        self.assertEqual(product.stock, 10)
        self.assertTrue(product.is_available())
    
    def test_negative_price_raises_error(self):
        """Test prix négatif lève exception."""
        with self.assertRaises(ValueError):
            Product(
                name="Invalid",
                price=Decimal("-10"),
                category=ProductCategory.ELECTRONICS
            )
    
    def test_decrease_stock(self):
        """Test diminution stock."""
        product = Product(
            name="Mouse",
            price=Decimal("29.99"),
            category=ProductCategory.ELECTRONICS,
            stock=20
        )
        
        product.decrease_stock(5)
        self.assertEqual(product.stock, 15)
    
    def test_decrease_stock_insufficient_raises_error(self):
        """Test stock insuffisant lève exception."""
        product = Product(
            name="Keyboard",
            price=Decimal("79.99"),
            category=ProductCategory.ELECTRONICS,
            stock=5
        )
        
        with self.assertRaises(ValueError):
            product.decrease_stock(10)


class TestShoppingCart(unittest.TestCase):
    """Tests unitaires pour ShoppingCart."""
    
    def setUp(self):
        """Préparation avant chaque test."""
        self.user = User(
            email="test@example.com",
            name="Test User",
            password_hash="hashed"
        )
        self.cart = ShoppingCart(self.user)
        
        self.product1 = Product(
            name="Product 1",
            price=Decimal("10.00"),
            category=ProductCategory.ELECTRONICS,
            stock=10
        )
        
        self.product2 = Product(
            name="Product 2",
            price=Decimal("20.00"),
            category=ProductCategory.ELECTRONICS,
            stock=5
        )
    
    def test_add_item(self):
        """Test ajout item."""
        self.cart.add_item(self.product1, 2)
        
        self.assertEqual(self.cart.item_count, 1)
        self.assertEqual(self.cart.total_quantity, 2)
    
    def test_calculate_total(self):
        """Test calcul total."""
        self.cart.add_item(self.product1, 2)  # 20€
        self.cart.add_item(self.product2, 1)  # 20€
        
        total = self.cart.calculate_total()
        self.assertEqual(total, Decimal("40.00"))
    
    def test_remove_item(self):
        """Test retrait item."""
        self.cart.add_item(self.product1, 1)
        self.cart.add_item(self.product2, 1)
        
        self.cart.remove_item(self.product1.id)
        
        self.assertEqual(self.cart.item_count, 1)
    
    def test_clear(self):
        """Test vider panier."""
        self.cart.add_item(self.product1, 1)
        self.cart.add_item(self.product2, 1)
        
        self.cart.clear()
        
        self.assertTrue(self.cart.is_empty())


class TestOrderService(unittest.TestCase):
    """Tests unitaires pour OrderService."""
    
    def setUp(self):
        """Préparation."""
        self.product_repo = InMemoryProductRepository()
        self.order_repo = InMemoryOrderRepository()
        
        self.service = OrderService(
            self.order_repo,
            self.product_repo
        )
        
        # Créer produit et utilisateur
        self.product = Product(
            name="Test Product",
            price=Decimal("50.00"),
            category=ProductCategory.ELECTRONICS,
            stock=10
        )
        self.product_repo.save(self.product)
        
        self.user = User(
            email="test@example.com",
            name="Test",
            password_hash="hash"
        )
        
        # Créer panier
        self.cart = ShoppingCart(self.user)
        self.cart.add_item(self.product, 2)
    
    def test_create_order_from_cart(self):
        """Test création commande depuis panier."""
        order = self.service.create_order_from_cart(self.cart)
        
        self.assertIsNotNone(order.id)
        self.assertEqual(order.status, OrderStatus.PENDING)
        self.assertEqual(order.total, Decimal("100.00"))
    
    def test_confirm_order_decreases_stock(self):
        """Test confirmation décrémente stock."""
        order = self.service.create_order_from_cart(self.cart)
        
        initial_stock = self.product.stock
        
        self.service.confirm_order(order.id)
        
        # Stock décrémenté
        self.assertEqual(
            self.product.stock,
            initial_stock - 2
        )
    
    def test_cancel_order_restores_stock(self):
        """Test annulation restaure stock."""
        order = self.service.create_order_from_cart(self.cart)
        self.service.confirm_order(order.id)
        
        stock_after_confirm = self.product.stock
        
        self.service.cancel_order(order.id)
        
        # Stock restauré
        self.assertEqual(
            self.product.stock,
            stock_after_confirm + 2
        )


"""
TESTS PYTEST
"""

@pytest.fixture
def product():
    """Fixture produit."""
    return Product(
        name="Test Product",
        price=Decimal("99.99"),
        category=ProductCategory.ELECTRONICS,
        stock=10
    )


@pytest.fixture
def user():
    """Fixture utilisateur."""
    return User(
        email="test@example.com",
        name="Test User",
        password_hash="hashed_password"
    )


@pytest.fixture
def cart(user):
    """Fixture panier."""
    return ShoppingCart(user)


def test_add_product_to_cart(cart, product):
    """Test ajout produit au panier (pytest)."""
    cart.add_item(product, 2)
    
    assert cart.item_count == 1
    assert cart.total_quantity == 2


def test_pricing_strategy_percentage_discount():
    """Test stratégie remise pourcentage."""
    strategy = PercentageDiscountStrategy(Decimal("10"))
    user = User("test@example.com", "Test", "hash")
    
    price = strategy.calculate_price(Decimal("100"), user)
    
    assert price == Decimal("90")


def test_order_state_transitions(user, cart, product):
    """Test transitions d'états commande."""
    cart.add_item(product, 1)
    order = Order(user, cart.items)
    
    # Pending -> Confirmed
    assert order.status == OrderStatus.PENDING
    order.confirm()
    assert order.status == OrderStatus.CONFIRMED
    
    # Confirmed -> Processing
    order.process()
    assert order.status == OrderStatus.PROCESSING
    
    # Processing -> Shipped
    order.ship()
    assert order.status == OrderStatus.SHIPPED
    
    # Shipped -> Delivered
    order.deliver()
    assert order.status == OrderStatus.DELIVERED


def test_command_pattern_undo(cart, product):
    """Test Pattern Command avec undo."""
    invoker = CommandInvoker()
    
    # Ajouter produit
    add_cmd = AddToCartCommand(cart, product, 1)
    invoker.execute(add_cmd)
    
    assert cart.item_count == 1
    
    # Undo
    invoker.undo()
    
    assert cart.is_empty()


"""
═══════════════════════════════════════════════════════════════════════════
APPLICATION COMPLÈTE - DÉMONSTRATION
═══════════════════════════════════════════════════════════════════════════
"""

def demo_ecommerce_system():
    """
    Démonstration complète du système e-commerce.
    
    Montre tous les patterns et fonctionnalités en action.
    """
    print("\n" + "="*70)
    print("[SHOPPING_TROLLEY] E-COMMERCE SYSTEM DEMONSTRATION")
    print("="*70)
    
    # ========================================================================
    # SETUP : Initialisation système
    # ========================================================================
    print("\n[PACKAGE] Setting up system...")
    
    # Repositories
    product_repo = InMemoryProductRepository()
    user_repo = InMemoryUserRepository()
    order_repo = InMemoryOrderRepository()
    
    # Services
    cart_service = CartService(product_repo)
    order_service = OrderService(order_repo, product_repo)
    payment_service = PaymentService()
    
    # Notifications (Pattern Observer)
    order_service.add_observer(EmailNotificationObserver())
    order_service.add_observer(SMSNotificationObserver())
    order_service.add_observer(AnalyticsObserver())
    
    # Facade
    checkout_facade = CheckoutFacade(
        cart_service,
        order_service,
        payment_service,
        product_repo
    )
    
    print("[OK] System initialized")
    
    # ========================================================================
    # CRÉER CATALOGUE (Pattern Factory)
    # ========================================================================
    print("\n[DOCS] Creating product catalog...")
    
    laptop = ProductFactory.create_product(
        ProductCategory.ELECTRONICS,
        "Dell XPS 15",
        Decimal("1299.99"),
        description="High-performance laptop",
        stock=5
    )
    
    mouse = ProductFactory.create_product(
        ProductCategory.ELECTRONICS,
        "Logitech MX Master",
        Decimal("99.99"),
        description="Wireless mouse",
        stock=20
    )
    
    tshirt = ProductFactory.create_product(
        ProductCategory.CLOTHING,
        "Blue T-Shirt",
        Decimal("19.99"),
        description="Cotton t-shirt",
        stock=100
    )
    
    # Sauvegarder produits
    product_repo.save(laptop)
    product_repo.save(mouse)
    product_repo.save(tshirt)
    
    print(f"[OK] Added {product_repo.find_all().__len__()} products")
    
    # ========================================================================
    # CRÉER UTILISATEUR
    # ========================================================================
    print("\n[UTILISATEUR] Creating user...")
    
    user = User(
        email="alice@example.com",
        name="Alice Dupont",
        password_hash="hashed_password_123"
    )
    user_repo.save(user)
    
    print(f"[OK] User created: {user.name}")
    
    # ========================================================================
    # CRÉER PANIER (Pattern Command pour historique)
    # ========================================================================
    print("\n[SHOPPING_TROLLEY] Building cart...")
    
    cart = ShoppingCart(user)
    invoker = CommandInvoker()
    
    # Ajouter produits avec Command Pattern
    add_laptop = AddToCartCommand(cart, laptop, 1)
    invoker.execute(add_laptop)
    
    add_mouse = AddToCartCommand(cart, mouse, 2)
    invoker.execute(add_mouse)
    
    add_tshirt = AddToCartCommand(cart, tshirt, 3)
    invoker.execute(add_tshirt)
    
    print(f"\nCart contents:")
    print(cart)
    
    # Démonstration undo
    print("\n<-  Undo last action (remove t-shirts)...")
    invoker.undo()
    
    print(f"\nCart after undo:")
    print(cart)
    
    # ========================================================================
    # APPLIQUER DÉCORATIONS (Pattern Decorator)
    # ========================================================================
    print("\n[WRAPPED_PRESENT] Applying decorations...")
    
    # Emballage cadeau pour laptop
    laptop_gift = GiftWrapDecorator(laptop)
    print(f"Laptop with gift wrap: {laptop_gift}")
    
    # Garantie pour laptop
    laptop_warranty = WarrantyDecorator(laptop, years=3)
    print(f"Laptop with warranty: {laptop_warranty}")
    
    # ========================================================================
    # CHECKOUT (Pattern Facade + Strategy)
    # ========================================================================
    print("\n[CARTE] Processing checkout...")
    
    # Stratégie de prix : 10% de remise
    pricing = PercentageDiscountStrategy(Decimal("10"))
    
    # Stratégie de livraison : express
    shipping = ExpressShippingStrategy()
    
    try:
        order, total = checkout_facade.checkout(
            cart=cart,
            pricing_strategy=pricing,
            shipping_strategy=shipping,
            payment_method="card",
            payment_details={"card_number": "****1234"}
        )
        
        print(f"\n[OK] Order placed successfully!")
        print(f"Order ID: {order.id}")
        print(f"Total paid: {total}€")
        
    except Exception as e:
        print(f"\n[X] Checkout failed: {e}")
        return
    
    # ========================================================================
    # SUIVI COMMANDE (Pattern State)
    # ========================================================================
    print("\n[PACKAGE] Order tracking...")
    
    print(f"\nCurrent status: {order.status.value}")
    
    # Traiter commande
    print("\n-> Processing order...")
    order_service.ship_order(order.id)
    
    print(f"Current status: {order.status.value}")
    
    # Livrer commande
    print("\n-> Delivering order...")
    order_service.deliver_order(order.id)
    
    print(f"Current status: {order.status.value}")
    
    # ========================================================================
    # HISTORIQUE UTILISATEUR
    # ========================================================================
    print("\n[LISTE] User order history...")
    
    user_orders = order_service.get_user_orders(user.id)
    
    print(f"\n{user.name} has {len(user_orders)} order(s):")
    for ord in user_orders:
        print(f"  - {ord.id}: {ord.status.value} ({ord.total}€)")
    
    # ========================================================================
    # STATISTIQUES
    # ========================================================================
    print("\n[GRAPHIQUE] System statistics...")
    
    total_products = len(product_repo.find_all())
    total_users = len(user_repo.find_all())
    total_orders = len(order_repo.find_all())
    
    print(f"Products: {total_products}")
    print(f"Users: {total_users}")
    print(f"Orders: {total_orders}")
    
    print("\n" + "="*70)
    print("[OK] DEMONSTRATION COMPLETE!")
    print("="*70 + "\n")


"""
═══════════════════════════════════════════════════════════════════════════
RÉCAPITULATIF PROJET E-COMMERCE
═══════════════════════════════════════════════════════════════════════════

PATTERNS UTILISÉS :
[OK] Singleton        -> AppConfig (configuration globale)
[OK] Factory Method   -> ProductFactory (création produits)
[OK] Abstract Factory -> NotificationFactory (familles notifications)
[OK] Prototype        -> ProductPrototype (clonage produits)
[OK] Builder          -> (OrderBuilder possible)
[OK] Adapter          -> (PaymentGatewayAdapter pour APIs)
[OK] Decorator        -> GiftWrap, ExpressShipping, Warranty
[OK] Facade           -> CheckoutFacade (simplification checkout)
[OK] Proxy            -> (LazyProductLoader possible)
[OK] Repository       -> ProductRepository, UserRepository, OrderRepository
[OK] Observer         -> EmailNotification, SMSNotification, Analytics
[OK] Strategy         -> PricingStrategy, ShippingStrategy
[OK] Command          -> AddToCart, RemoveFromCart (avec undo/redo)
[OK] State            -> OrderState (6 états : Pending -> Delivered)
[OK] Template Method  -> (OrderProcessor possible)


PRINCIPES SOLID :
[OK] SRP : Chaque classe une responsabilité
[OK] OCP : Extensible via strategies, observers
[OK] LSP : Substitution strategies, states
[OK] ISP : Interfaces ciblées (Repository, Observer)
[OK] DIP : Services dépendent d'abstractions


ARCHITECTURE :
[OK] Domain Layer     -> Product, User, Order, Cart
[OK] Service Layer    -> CartService, OrderService, PaymentService
[OK] Repository Layer -> Abstraction persistance
[OK] Infrastructure   -> Implémentations (InMemory)


TESTS :
[OK] Tests unitaires (unittest)
[OK] Tests avec fixtures (pytest)
[OK] Mocks pour dépendances
[OK] Tests d'intégration


Ce projet démontre :
- Application pratique de TOUS les patterns
- Architecture propre et maintenable
- Code testable et évolutif
- Séparation des responsabilités
- Principes SOLID en pratique
"""


if __name__ == '__main__':
    # Exécuter démonstration
    demo_ecommerce_system()
    
    # Ou exécuter tests
    # unittest.main()
    # pytest.main([__file__])


# ============================================================================
# [BANQUE] PROJET PRATIQUE 2 : SYSTÈME BANCAIRE PROFESSIONNEL
# ============================================================================
#
# [OBJECTIF] OBJECTIF :
# Créer un système bancaire avec Architecture Hexagonale et Event Sourcing :
# - Architecture Hexagonale (Ports & Adapters)
# - Event Sourcing (historique événements)
# - CQRS (Command Query Responsibility Segregation)
# - Domain-Driven Design (DDD)
# - TDD strict (Test Driven Development)
# - SOLID à 100%
#
# [DOCS] CONCEPTS APPLIQUÉS :
# [OK] Architecture Hexagonale
# [OK] Event Sourcing
# [OK] CQRS Pattern
# [OK] Domain Events
# [OK] Aggregates et Value Objects (DDD)
# [OK] TDD complet
# [OK] SOLID parfait
#
# [TEMPS] TEMPS : ~10-12 heures de lecture et pratique
# [IDEE] NIVEAU : Avancé - Architecture professionnelle
# ============================================================================


"""
═══════════════════════════════════════════════════════════════════════════
[CONSTRUCTION] ARCHITECTURE HEXAGONALE (PORTS & ADAPTERS)
═══════════════════════════════════════════════════════════════════════════

[IDEE] ARCHITECTURE HEXAGONALE

Principe :
- Domaine au centre (pur, sans dépendances)
- Ports (interfaces) définissent contrats
- Adapters implémentent ports (infrastructure)
- Dépendances pointent vers le domaine (DIP)


Structure du projet :

banking_system/
├── domain/                     # CŒUR (0 dépendance externe)
│   ├── model/                 # Entités, Value Objects, Aggregates
│   │   ├── account.py
│   │   ├── transaction.py
│   │   ├── customer.py
│   │   └── value_objects.py
│   ├── events/                # Domain Events
│   │   └── account_events.py
│   ├── commands/              # Commands (CQRS)
│   │   └── account_commands.py
│   └── ports/                 # Interfaces (contrats)
│       ├── repositories.py    # Port sortant
│       └── event_store.py     # Port sortant
├── application/               # USE CASES
│   ├── commands/              # Command Handlers
│   │   └── account_handlers.py
│   └── queries/               # Query Handlers
│       └── account_queries.py
├── infrastructure/            # ADAPTERS (implémentations)
│   ├── persistence/
│   │   ├── in_memory_repository.py
│   │   └── sql_repository.py
│   ├── event_store/
│   │   └── in_memory_event_store.py
│   └── messaging/
│       └── event_publisher.py
└── tests/
    ├── unit/
    ├── integration/
    └── acceptance/


AVANTAGES :
[OK] Domaine isolé (testable sans infrastructure)
[OK] Flexibilité (changer DB/UI sans toucher domaine)
[OK] Testabilité maximale
[OK] Principe DIP respecté
"""


# ============================================================================
# [PACKAGE] PARTIE 1 : DOMAIN LAYER (CŒUR MÉTIER)
# ============================================================================

from __future__ import annotations
from typing import List, Optional, Dict, Protocol
from datetime import datetime, date
from decimal import Decimal
from enum import Enum
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
import uuid


"""
═══════════════════════════════════════════════════════════════════════════
VALUE OBJECTS (DDD)
═══════════════════════════════════════════════════════════════════════════

[IDEE] VALUE OBJECT

Caractéristiques :
- Immuable
- Égalité par valeur (pas identité)
- Pas d'identité propre
- Valide à la construction


Exemples : Money, Email, AccountNumber, IBAN
"""

@dataclass(frozen=True)
class Money:
    """
    [IDEE] VALUE OBJECT : Montant d'argent
    
    Immuable, valide à la construction.
    Encapsule devise et montant.
    
    Attributes:
        amount: Montant (Decimal pour précision)
        currency: Code devise (EUR, USD, etc.)
    
    Example:
        >>> price = Money(Decimal("100.50"), "EUR")
        >>> tax = Money(Decimal("20.10"), "EUR")
        >>> total = price + tax
        >>> print(total)
        120.60 EUR
    """
    
    amount: Decimal
    currency: str = "EUR"
    
    def __post_init__(self):
        """
        Validation à la construction.
        
        Raises:
            ValueError: Si montant ou devise invalide
        """
        if not isinstance(self.amount, Decimal):
            raise TypeError("Amount must be Decimal")
        
        if not self.currency or len(self.currency) != 3:
            raise ValueError("Currency must be 3-letter code (e.g., EUR)")
    
    def __add__(self, other: Money) -> Money:
        """
        Addition de deux montants.
        
        Args:
            other: Autre montant
            
        Returns:
            Nouveau Money (immuable)
            
        Raises:
            ValueError: Si devises différentes
        """
        if self.currency != other.currency:
            raise ValueError(
                f"Cannot add {self.currency} and {other.currency}"
            )
        
        return Money(self.amount + other.amount, self.currency)
    
    def __sub__(self, other: Money) -> Money:
        """Soustraction."""
        if self.currency != other.currency:
            raise ValueError(
                f"Cannot subtract {other.currency} from {self.currency}"
            )
        
        return Money(self.amount - other.amount, self.currency)
    
    def __mul__(self, multiplier: Decimal) -> Money:
        """Multiplication."""
        return Money(self.amount * multiplier, self.currency)
    
    def __truediv__(self, divisor: Decimal) -> Money:
        """Division."""
        return Money(self.amount / divisor, self.currency)
    
    def __lt__(self, other: Money) -> bool:
        """Comparaison <"""
        if self.currency != other.currency:
            raise ValueError("Cannot compare different currencies")
        return self.amount < other.amount
    
    def __le__(self, other: Money) -> bool:
        """Comparaison <="""
        if self.currency != other.currency:
            raise ValueError("Cannot compare different currencies")
        return self.amount <= other.amount
    
    def __gt__(self, other: Money) -> bool:
        """Comparaison >"""
        if self.currency != other.currency:
            raise ValueError("Cannot compare different currencies")
        return self.amount > other.amount
    
    def __ge__(self, other: Money) -> bool:
        """Comparaison >="""
        if self.currency != other.currency:
            raise ValueError("Cannot compare different currencies")
        return self.amount >= other.amount
    
    def is_positive(self) -> bool:
        """Vérifier si montant positif."""
        return self.amount > 0
    
    def is_negative(self) -> bool:
        """Vérifier si montant négatif."""
        return self.amount < 0
    
    def is_zero(self) -> bool:
        """Vérifier si montant zéro."""
        return self.amount == 0
    
    def abs(self) -> Money:
        """Valeur absolue."""
        return Money(abs(self.amount), self.currency)
    
    def negate(self) -> Money:
        """Négation."""
        return Money(-self.amount, self.currency)
    
    def __str__(self) -> str:
        """Représentation utilisateur."""
        return f"{self.amount:.2f} {self.currency}"
    
    def __repr__(self) -> str:
        """Représentation développeur."""
        return f"Money({self.amount!r}, {self.currency!r})"
    
    @classmethod
    def zero(cls, currency: str = "EUR") -> Money:
        """Créer montant zéro."""
        return cls(Decimal("0"), currency)


@dataclass(frozen=True)
class AccountNumber:
    """
    [IDEE] VALUE OBJECT : Numéro de compte
    
    Format : XX-XXXXXXXX (ex: FR-12345678)
    """
    
    value: str
    
    def __post_init__(self):
        """Validation numéro de compte."""
        if not self.value or len(self.value) < 5:
            raise ValueError("Invalid account number")
        
        # Format : XX-XXXXXXXX
        if '-' not in self.value:
            raise ValueError("Account number must contain hyphen")
    
    def __str__(self) -> str:
        return self.value
    
    @classmethod
    def generate(cls, country_code: str = "FR") -> AccountNumber:
        """
        Générer numéro de compte aléatoire.
        
        Args:
            country_code: Code pays (2 lettres)
            
        Returns:
            Nouveau numéro de compte
        """
        import random
        number = ''.join(str(random.randint(0, 9)) for _ in range(8))
        return cls(f"{country_code}-{number}")


@dataclass(frozen=True)
class CustomerId:
    """
    [IDEE] VALUE OBJECT : Identifiant client
    """
    
    value: str
    
    def __post_init__(self):
        """Validation."""
        if not self.value:
            raise ValueError("CustomerId cannot be empty")
    
    def __str__(self) -> str:
        return self.value
    
    @classmethod
    def generate(cls) -> CustomerId:
        """Générer nouvel ID."""
        return cls(str(uuid.uuid4()))


@dataclass(frozen=True)
class Email:
    """
    [IDEE] VALUE OBJECT : Adresse email
    """
    
    value: str
    
    def __post_init__(self):
        """Validation email."""
        if not self.value or '@' not in self.value:
            raise ValueError(f"Invalid email: {self.value}")
        
        # Normalisation
        object.__setattr__(self, 'value', self.value.lower().strip())
    
    def __str__(self) -> str:
        return self.value


"""
═══════════════════════════════════════════════════════════════════════════
DOMAIN EVENTS (Event Sourcing)
═══════════════════════════════════════════════════════════════════════════

[IDEE] DOMAIN EVENT

Représente fait métier qui s'est produit.
Immuable, passé (verbe au passé).


EVENT SOURCING :
Au lieu de stocker l'état actuel, on stocke la séquence d'événements.
État actuel = réduction de tous les événements.
"""

@dataclass(frozen=True)
class DomainEvent(ABC):
    """
    [IDEE] DOMAIN EVENT
    
    Événement métier immuable.
    Base pour tous les événements du domaine.
    """
    
    event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    occurred_at: datetime = field(default_factory=datetime.now)
    
    @abstractmethod
    def event_type(self) -> str:
        """Type d'événement (pour sérialisation)."""
        pass


@dataclass(frozen=True)
class AccountOpenedEvent(DomainEvent):
    """
    Événement : Compte ouvert.
    
    Attributs métier :
    - account_number: Numéro de compte
    - customer_id: ID client
    - initial_balance: Solde initial
    """
    
    account_number: AccountNumber
    customer_id: CustomerId
    initial_balance: Money
    account_type: str  # "checking" ou "savings"
    
    def event_type(self) -> str:
        return "AccountOpened"


@dataclass(frozen=True)
class MoneyDepositedEvent(DomainEvent):
    """Événement : Dépôt effectué."""
    
    account_number: AccountNumber
    amount: Money
    description: str = ""
    
    def event_type(self) -> str:
        return "MoneyDeposited"


@dataclass(frozen=True)
class MoneyWithdrawnEvent(DomainEvent):
    """Événement : Retrait effectué."""
    
    account_number: AccountNumber
    amount: Money
    description: str = ""
    
    def event_type(self) -> str:
        return "MoneyWithdrawn"


@dataclass(frozen=True)
class MoneyTransferredEvent(DomainEvent):
    """Événement : Virement effectué."""
    
    from_account: AccountNumber
    to_account: AccountNumber
    amount: Money
    description: str = ""
    
    def event_type(self) -> str:
        return "MoneyTransferred"


@dataclass(frozen=True)
class AccountClosedEvent(DomainEvent):
    """Événement : Compte fermé."""
    
    account_number: AccountNumber
    reason: str = ""
    
    def event_type(self) -> str:
        return "AccountClosed"


@dataclass(frozen=True)
class AccountFrozenEvent(DomainEvent):
    """Événement : Compte gelé."""
    
    account_number: AccountNumber
    reason: str
    
    def event_type(self) -> str:
        return "AccountFrozen"


@dataclass(frozen=True)
class AccountUnfrozenEvent(DomainEvent):
    """Événement : Compte dégelé."""
    
    account_number: AccountNumber
    
    def event_type(self) -> str:
        return "AccountUnfrozen"


"""
═══════════════════════════════════════════════════════════════════════════
AGGREGATE ROOT : ACCOUNT (DDD + Event Sourcing)
═══════════════════════════════════════════════════════════════════════════

[IDEE] AGGREGATE

Groupe d'objets traités comme une unité.
Garantit cohérence et invariants.
Une racine (root) pour l'accès externe.


[IDEE] EVENT SOURCING

État reconstruit depuis événements.
Méthodes :
- Commands -> décident si événement doit être créé
- Events -> modifient état (apply)
"""

class AccountStatus(Enum):
    """Statut du compte."""
    ACTIVE = "active"
    FROZEN = "frozen"
    CLOSED = "closed"


class AccountType(Enum):
    """Type de compte."""
    CHECKING = "checking"  # Compte courant
    SAVINGS = "savings"    # Compte épargne


class Account:
    """
    [IDEE] AGGREGATE ROOT : Compte bancaire
    
    Utilise Event Sourcing :
    - État reconstruit depuis événements
    - Commandes créent événements
    - apply() modifie état
    
    Invariants garantis :
    - Solde jamais négatif
    - Compte fermé ne peut pas être modifié
    - Compte gelé ne permet que consultations
    """
    
    def __init__(self, account_number: AccountNumber):
        """
        Initialise compte (vide).
        
        État sera reconstruit via apply() des événements.
        """
        # Identité
        self._account_number = account_number
        
        # État (reconstruit depuis événements)
        self._customer_id: Optional[CustomerId] = None
        self._balance: Money = Money.zero()
        self._account_type: Optional[AccountType] = None
        self._status: AccountStatus = AccountStatus.ACTIVE
        self._opened_at: Optional[datetime] = None
        self._closed_at: Optional[datetime] = None
        
        # Event Sourcing
        self._version: int = 0  # Version pour optimistic locking
        self._uncommitted_events: List[DomainEvent] = []
    
    # ========================================================================
    # PROPERTIES (lecture seule)
    # ========================================================================
    
    @property
    def account_number(self) -> AccountNumber:
        """Numéro de compte."""
        return self._account_number
    
    @property
    def customer_id(self) -> Optional[CustomerId]:
        """ID du client propriétaire."""
        return self._customer_id
    
    @property
    def balance(self) -> Money:
        """Solde actuel."""
        return self._balance
    
    @property
    def account_type(self) -> Optional[AccountType]:
        """Type de compte."""
        return self._account_type
    
    @property
    def status(self) -> AccountStatus:
        """Statut du compte."""
        return self._status
    
    @property
    def is_active(self) -> bool:
        """Compte actif ?"""
        return self._status == AccountStatus.ACTIVE
    
    @property
    def is_frozen(self) -> bool:
        """Compte gelé ?"""
        return self._status == AccountStatus.FROZEN
    
    @property
    def is_closed(self) -> bool:
        """Compte fermé ?"""
        return self._status == AccountStatus.CLOSED
    
    @property
    def version(self) -> int:
        """Version (Event Sourcing)."""
        return self._version
    
    # ========================================================================
    # COMMANDS (décisions métier -> événements)
    # ========================================================================
    
    @staticmethod
    def open(
        account_number: AccountNumber,
        customer_id: CustomerId,
        account_type: AccountType,
        initial_deposit: Money = None
    ) -> Account:
        """
        [IDEE] FACTORY METHOD : Ouvrir nouveau compte
        
        Args:
            account_number: Numéro de compte
            customer_id: ID client
            account_type: Type de compte
            initial_deposit: Dépôt initial optionnel
            
        Returns:
            Nouveau compte
            
        Raises:
            ValueError: Si dépôt initial négatif
        """
        account = Account(account_number)
        
        # Validation
        if initial_deposit is None:
            initial_deposit = Money.zero()
        
        if initial_deposit.is_negative():
            raise ValueError("Initial deposit cannot be negative")
        
        # Créer événement
        event = AccountOpenedEvent(
            account_number=account_number,
            customer_id=customer_id,
            initial_balance=initial_deposit,
            account_type=account_type.value
        )
        
        # Appliquer événement
        account._apply_event(event)
        
        return account
    
    def deposit(self, amount: Money, description: str = "") -> None:
        """
        [IDEE] COMMAND : Déposer de l'argent
        
        Args:
            amount: Montant à déposer
            description: Description optionnelle
            
        Raises:
            ValueError: Si compte fermé ou montant invalide
        """
        # Validations métier (invariants)
        self._ensure_not_closed()
        self._ensure_not_frozen()
        
        if not amount.is_positive():
            raise ValueError("Deposit amount must be positive")
        
        if amount.currency != self._balance.currency:
            raise ValueError("Currency mismatch")
        
        # Créer événement
        event = MoneyDepositedEvent(
            account_number=self._account_number,
            amount=amount,
            description=description
        )
        
        # Appliquer
        self._apply_event(event)
    
    def withdraw(self, amount: Money, description: str = "") -> None:
        """
        [IDEE] COMMAND : Retirer de l'argent
        
        Args:
            amount: Montant à retirer
            description: Description
            
        Raises:
            ValueError: Si solde insuffisant ou compte invalide
        """
        # Validations
        self._ensure_not_closed()
        self._ensure_not_frozen()
        
        if not amount.is_positive():
            raise ValueError("Withdrawal amount must be positive")
        
        if amount.currency != self._balance.currency:
            raise ValueError("Currency mismatch")
        
        # Invariant : solde jamais négatif
        if self._balance < amount:
            raise ValueError(
                f"Insufficient funds. Balance: {self._balance}, "
                f"Requested: {amount}"
            )
        
        # Créer événement
        event = MoneyWithdrawnEvent(
            account_number=self._account_number,
            amount=amount,
            description=description
        )
        
        # Appliquer
        self._apply_event(event)
    
    def freeze(self, reason: str) -> None:
        """
        Geler compte.
        
        Args:
            reason: Raison du gel
        """
        self._ensure_not_closed()
        
        if self.is_frozen:
            return  # Déjà gelé
        
        event = AccountFrozenEvent(
            account_number=self._account_number,
            reason=reason
        )
        
        self._apply_event(event)
    
    def unfreeze(self) -> None:
        """Dégeler compte."""
        self._ensure_not_closed()
        
        if not self.is_frozen:
            return  # Pas gelé
        
        event = AccountUnfrozenEvent(
            account_number=self._account_number
        )
        
        self._apply_event(event)
    
    def close(self, reason: str = "") -> None:
        """
        Fermer compte.
        
        Args:
            reason: Raison de fermeture
            
        Raises:
            ValueError: Si solde non nul
        """
        if self.is_closed:
            return  # Déjà fermé
        
        # Invariant : solde doit être zéro pour fermer
        if not self._balance.is_zero():
            raise ValueError(
                f"Cannot close account with non-zero balance: {self._balance}"
            )
        
        event = AccountClosedEvent(
            account_number=self._account_number,
            reason=reason
        )
        
        self._apply_event(event)
    
    # ========================================================================
    # EVENT SOURCING : Apply events
    # ========================================================================
    
    def _apply_event(self, event: DomainEvent, is_new: bool = True) -> None:
        """
        Appliquer événement (modifie état).
        
        Args:
            event: Événement à appliquer
            is_new: True si événement nouvellement créé
        """
        # Dispatcher vers méthode spécifique
        method_name = f"_apply_{event.event_type()}"
        
        if hasattr(self, method_name):
            method = getattr(self, method_name)
            method(event)
        else:
            raise ValueError(f"Unknown event type: {event.event_type()}")
        
        # Incrémenter version
        self._version += 1
        
        # Si nouvel événement, l'ajouter à la liste des uncommitted
        if is_new:
            self._uncommitted_events.append(event)
    
    def _apply_AccountOpened(self, event: AccountOpenedEvent) -> None:
        """Appliquer AccountOpenedEvent."""
        self._customer_id = event.customer_id
        self._balance = event.initial_balance
        self._account_type = AccountType(event.account_type)
        self._status = AccountStatus.ACTIVE
        self._opened_at = event.occurred_at
    
    def _apply_MoneyDeposited(self, event: MoneyDepositedEvent) -> None:
        """Appliquer MoneyDepositedEvent."""
        self._balance = self._balance + event.amount
    
    def _apply_MoneyWithdrawn(self, event: MoneyWithdrawnEvent) -> None:
        """Appliquer MoneyWithdrawnEvent."""
        self._balance = self._balance - event.amount
    
    def _apply_AccountFrozen(self, event: AccountFrozenEvent) -> None:
        """Appliquer AccountFrozenEvent."""
        self._status = AccountStatus.FROZEN
    
    def _apply_AccountUnfrozen(self, event: AccountUnfrozenEvent) -> None:
        """Appliquer AccountUnfrozenEvent."""
        self._status = AccountStatus.ACTIVE
    
    def _apply_AccountClosed(self, event: AccountClosedEvent) -> None:
        """Appliquer AccountClosedEvent."""
        self._status = AccountStatus.CLOSED
        self._closed_at = event.occurred_at
    
    # ========================================================================
    # Event Sourcing : Reconstruction
    # ========================================================================
    
    @classmethod
    def from_events(
        cls,
        account_number: AccountNumber,
        events: List[DomainEvent]
    ) -> Account:
        """
        [IDEE] RECONSTRUCTION depuis événements
        
        Args:
            account_number: Numéro de compte
            events: Liste d'événements (ordre chronologique)
            
        Returns:
            Compte avec état reconstitué
        """
        account = cls(account_number)
        
        for event in events:
            account._apply_event(event, is_new=False)
        
        return account
    
    def get_uncommitted_events(self) -> List[DomainEvent]:
        """Récupérer événements non encore persistés."""
        return self._uncommitted_events.copy()
    
    def mark_events_as_committed(self) -> None:
        """Marquer événements comme persistés."""
        self._uncommitted_events.clear()
    
    # ========================================================================
    # Validations internes
    # ========================================================================
    
    def _ensure_not_closed(self) -> None:
        """Vérifier que compte pas fermé."""
        if self.is_closed:
            raise ValueError(f"Account {self._account_number} is closed")
    
    def _ensure_not_frozen(self) -> None:
        """Vérifier que compte pas gelé."""
        if self.is_frozen:
            raise ValueError(f"Account {self._account_number} is frozen")
    
    # ========================================================================
    # Représentation
    # ========================================================================
    
    def __repr__(self) -> str:
        return (
            f"Account({self._account_number}, "
            f"balance={self._balance}, status={self._status.value})"
        )
    
    def __str__(self) -> str:
        return (
            f"Account {self._account_number}\n"
            f"Type: {self._account_type.value if self._account_type else 'N/A'}\n"
            f"Balance: {self._balance}\n"
            f"Status: {self._status.value}\n"
            f"Version: {self._version}"
        )


# Ce fichier continue...
# ============================================================================

# ============================================================================
# [BANQUE] PROJET PRATIQUE 2 : SYSTÈME BANCAIRE (PARTIE 2)
# ============================================================================
#
# Suite du projet : Ports, CQRS, TDD
#
# [OBJECTIF] CONTENU :
# - Ports (interfaces hexagonales)
# - CQRS : Commands et Command Handlers
# - CQRS : Queries et Query Handlers
# - TDD : Tests avant implémentation
# - Transaction domain service
#
# ============================================================================


# Suite de la Partie 1...

from typing import List, Optional, Protocol, runtime_checkable
from abc import ABC, abstractmethod
from decimal import Decimal
from datetime import datetime
import pytest


"""
═══════════════════════════════════════════════════════════════════════════
PORTS (Architecture Hexagonale)
═══════════════════════════════════════════════════════════════════════════

[IDEE] PORTS

Interfaces définissant contrats entre domaine et infrastructure.

Deux types :
1. PRIMARY PORTS (entrants) : Use cases exposés
2. SECONDARY PORTS (sortants) : Services externes requis
"""

# ============================================================================
# SECONDARY PORTS (sortants) : Ce que le domaine REQUIERT
# ============================================================================

@runtime_checkable
class EventStore(Protocol):
    """
    [IDEE] PORT : Event Store
    
    Stockage des événements (Event Sourcing).
    Infrastructure doit implémenter ce contrat.
    """
    
    def save_events(
        self,
        aggregate_id: str,
        events: List[DomainEvent],
        expected_version: int
    ) -> None:
        """
        Sauvegarder événements.
        
        Args:
            aggregate_id: ID de l'aggregate
            events: Événements à sauvegarder
            expected_version: Version attendue (optimistic locking)
            
        Raises:
            ConcurrencyException: Si version ne correspond pas
        """
        ...
    
    def get_events(
        self,
        aggregate_id: str,
        from_version: int = 0
    ) -> List[DomainEvent]:
        """
        Récupérer événements d'un aggregate.
        
        Args:
            aggregate_id: ID de l'aggregate
            from_version: Version de départ
            
        Returns:
            Liste d'événements (ordre chronologique)
        """
        ...


@runtime_checkable
class AccountRepository(Protocol):
    """
    [IDEE] PORT : Repository des comptes
    
    Accès aux aggregates Account.
    """
    
    def save(self, account: Account) -> None:
        """
        Sauvegarder compte.
        
        Args:
            account: Compte à sauvegarder
        """
        ...
    
    def get_by_account_number(
        self,
        account_number: AccountNumber
    ) -> Optional[Account]:
        """
        Récupérer compte par numéro.
        
        Args:
            account_number: Numéro de compte
            
        Returns:
            Compte si trouvé, None sinon
        """
        ...
    
    def exists(self, account_number: AccountNumber) -> bool:
        """
        Vérifier si compte existe.
        
        Args:
            account_number: Numéro de compte
            
        Returns:
            True si existe
        """
        ...


@runtime_checkable
class EventPublisher(Protocol):
    """
    [IDEE] PORT : Publication d'événements
    
    Notifier autres services des événements domaine.
    """
    
    def publish(self, event: DomainEvent) -> None:
        """
        Publier événement.
        
        Args:
            event: Événement à publier
        """
        ...


"""
═══════════════════════════════════════════════════════════════════════════
DOMAIN SERVICES
═══════════════════════════════════════════════════════════════════════════

[IDEE] DOMAIN SERVICE

Logique métier qui ne rentre pas naturellement dans une entité.
Opérations impliquant plusieurs aggregates.
"""

class TransferService:
    """
    [IDEE] DOMAIN SERVICE : Virement entre comptes
    
    Coordination entre deux comptes (deux aggregates).
    Garantit atomicité du virement.
    """
    
    def __init__(self, repository: AccountRepository):
        """
        Initialise service.
        
        Args:
            repository: Repository des comptes (DIP)
        """
        self.repository = repository
    
    def transfer(
        self,
        from_account_number: AccountNumber,
        to_account_number: AccountNumber,
        amount: Money,
        description: str = ""
    ) -> None:
        """
        Effectuer virement.
        
        Args:
            from_account_number: Compte source
            to_account_number: Compte destination
            amount: Montant
            description: Description
            
        Raises:
            ValueError: Si comptes introuvables ou virement impossible
        """
        # Récupérer comptes
        from_account = self.repository.get_by_account_number(from_account_number)
        if not from_account:
            raise ValueError(f"Source account {from_account_number} not found")
        
        to_account = self.repository.get_by_account_number(to_account_number)
        if not to_account:
            raise ValueError(f"Destination account {to_account_number} not found")
        
        # Validation métier
        if from_account_number == to_account_number:
            raise ValueError("Cannot transfer to same account")
        
        # Effectuer virement (atomique dans aggregate)
        # 1. Retirer de source
        from_account.withdraw(amount, f"Transfer to {to_account_number}: {description}")
        
        # 2. Déposer dans destination
        to_account.deposit(amount, f"Transfer from {from_account_number}: {description}")
        
        # 3. Sauvegarder les deux comptes
        self.repository.save(from_account)
        self.repository.save(to_account)
        
        # Note : En production, utiliser Unit of Work pattern
        # pour garantir atomicité transaction


"""
═══════════════════════════════════════════════════════════════════════════
CQRS : COMMANDS
═══════════════════════════════════════════════════════════════════════════

[IDEE] CQRS (Command Query Responsibility Segregation)

Séparer :
- COMMANDS : Modifient état (write model)
- QUERIES : Lisent données (read model)


Avantages :
[OK] Scalabilité (read/write séparés)
[OK] Optimisation différente pour lecture/écriture
[OK] Clarté intentions
"""

from dataclasses import dataclass

@dataclass(frozen=True)
class Command(ABC):
    """
    [IDEE] COMMAND (CQRS)
    
    Intention de modifier l'état.
    Immuable, contient toutes les données nécessaires.
    """
    pass


@dataclass(frozen=True)
class OpenAccountCommand(Command):
    """
    Command : Ouvrir nouveau compte.
    
    Attributes:
        customer_id: ID client
        account_type: Type de compte
        initial_deposit: Dépôt initial (optionnel)
    """
    customer_id: str
    account_type: str  # "checking" ou "savings"
    initial_deposit: Optional[Decimal] = None
    currency: str = "EUR"


@dataclass(frozen=True)
class DepositMoneyCommand(Command):
    """Command : Déposer argent."""
    account_number: str
    amount: Decimal
    currency: str = "EUR"
    description: str = ""


@dataclass(frozen=True)
class WithdrawMoneyCommand(Command):
    """Command : Retirer argent."""
    account_number: str
    amount: Decimal
    currency: str = "EUR"
    description: str = ""


@dataclass(frozen=True)
class TransferMoneyCommand(Command):
    """Command : Virer argent."""
    from_account_number: str
    to_account_number: str
    amount: Decimal
    currency: str = "EUR"
    description: str = ""


@dataclass(frozen=True)
class FreezeAccountCommand(Command):
    """Command : Geler compte."""
    account_number: str
    reason: str


@dataclass(frozen=True)
class UnfreezeAccountCommand(Command):
    """Command : Dégeler compte."""
    account_number: str


@dataclass(frozen=True)
class CloseAccountCommand(Command):
    """Command : Fermer compte."""
    account_number: str
    reason: str = ""


"""
═══════════════════════════════════════════════════════════════════════════
COMMAND HANDLERS (Application Layer)
═══════════════════════════════════════════════════════════════════════════

[IDEE] COMMAND HANDLER

Traite command, orchestre domaine.
Application layer (use cases).
"""

class CommandHandler(ABC):
    """Handler de commande abstrait."""
    
    @abstractmethod
    def handle(self, command: Command) -> None:
        """
        Traiter commande.
        
        Args:
            command: Commande à traiter
        """
        pass


class OpenAccountHandler(CommandHandler):
    """Handler : Ouvrir compte."""
    
    def __init__(
        self,
        repository: AccountRepository,
        event_publisher: EventPublisher
    ):
        """
        Initialise handler.
        
        Args:
            repository: Repository comptes (DIP)
            event_publisher: Publisher événements (DIP)
        """
        self.repository = repository
        self.event_publisher = event_publisher
    
    def handle(self, command: OpenAccountCommand) -> AccountNumber:
        """
        Traiter OpenAccountCommand.
        
        Args:
            command: Commande
            
        Returns:
            Numéro de compte créé
            
        Raises:
            ValueError: Si données invalides
        """
        # Générer numéro de compte
        account_number = AccountNumber.generate()
        
        # Vérifier que compte n'existe pas déjà
        if self.repository.exists(account_number):
            raise ValueError(f"Account {account_number} already exists")
        
        # Convertir données
        customer_id = CustomerId(command.customer_id)
        account_type = AccountType(command.account_type)
        
        initial_deposit = None
        if command.initial_deposit is not None:
            initial_deposit = Money(command.initial_deposit, command.currency)
        
        # Créer compte (aggregate)
        account = Account.open(
            account_number=account_number,
            customer_id=customer_id,
            account_type=account_type,
            initial_deposit=initial_deposit
        )
        
        # Sauvegarder
        self.repository.save(account)
        
        # Publier événements
        for event in account.get_uncommitted_events():
            self.event_publisher.publish(event)
        
        account.mark_events_as_committed()
        
        return account_number


class DepositMoneyHandler(CommandHandler):
    """Handler : Déposer argent."""
    
    def __init__(
        self,
        repository: AccountRepository,
        event_publisher: EventPublisher
    ):
        self.repository = repository
        self.event_publisher = event_publisher
    
    def handle(self, command: DepositMoneyCommand) -> None:
        """Traiter DepositMoneyCommand."""
        # Récupérer compte
        account_number = AccountNumber(command.account_number)
        account = self.repository.get_by_account_number(account_number)
        
        if not account:
            raise ValueError(f"Account {account_number} not found")
        
        # Déposer
        amount = Money(command.amount, command.currency)
        account.deposit(amount, command.description)
        
        # Sauvegarder
        self.repository.save(account)
        
        # Publier événements
        for event in account.get_uncommitted_events():
            self.event_publisher.publish(event)
        
        account.mark_events_as_committed()


class WithdrawMoneyHandler(CommandHandler):
    """Handler : Retirer argent."""
    
    def __init__(
        self,
        repository: AccountRepository,
        event_publisher: EventPublisher
    ):
        self.repository = repository
        self.event_publisher = event_publisher
    
    def handle(self, command: WithdrawMoneyCommand) -> None:
        """Traiter WithdrawMoneyCommand."""
        # Récupérer compte
        account_number = AccountNumber(command.account_number)
        account = self.repository.get_by_account_number(account_number)
        
        if not account:
            raise ValueError(f"Account {account_number} not found")
        
        # Retirer
        amount = Money(command.amount, command.currency)
        account.withdraw(amount, command.description)
        
        # Sauvegarder
        self.repository.save(account)
        
        # Publier événements
        for event in account.get_uncommitted_events():
            self.event_publisher.publish(event)
        
        account.mark_events_as_committed()


class TransferMoneyHandler(CommandHandler):
    """Handler : Virer argent."""
    
    def __init__(
        self,
        transfer_service: TransferService,
        event_publisher: EventPublisher
    ):
        self.transfer_service = transfer_service
        self.event_publisher = event_publisher
    
    def handle(self, command: TransferMoneyCommand) -> None:
        """Traiter TransferMoneyCommand."""
        from_account = AccountNumber(command.from_account_number)
        to_account = AccountNumber(command.to_account_number)
        amount = Money(command.amount, command.currency)
        
        # Utiliser domain service
        self.transfer_service.transfer(
            from_account,
            to_account,
            amount,
            command.description
        )


"""
═══════════════════════════════════════════════════════════════════════════
CQRS : QUERIES (Read Model)
═══════════════════════════════════════════════════════════════════════════
"""

@dataclass(frozen=True)
class Query(ABC):
    """Query abstraite (CQRS)."""
    pass


@dataclass(frozen=True)
class GetAccountBalanceQuery(Query):
    """Query : Obtenir solde compte."""
    account_number: str


@dataclass(frozen=True)
class GetAccountHistoryQuery(Query):
    """Query : Obtenir historique compte."""
    account_number: str
    from_date: Optional[datetime] = None
    to_date: Optional[datetime] = None


@dataclass(frozen=True)
class AccountBalanceDTO:
    """
    DTO (Data Transfer Object) : Solde compte.
    
    Read model optimisé pour lecture.
    """
    account_number: str
    balance: Decimal
    currency: str
    status: str


@dataclass(frozen=True)
class TransactionDTO:
    """DTO : Transaction."""
    timestamp: datetime
    type: str  # "deposit", "withdrawal", "transfer"
    amount: Decimal
    currency: str
    description: str
    balance_after: Decimal


class QueryHandler(ABC):
    """Handler de query abstrait."""
    
    @abstractmethod
    def handle(self, query: Query):
        """Traiter query."""
        pass


class GetAccountBalanceHandler(QueryHandler):
    """Handler : Obtenir solde."""
    
    def __init__(self, repository: AccountRepository):
        self.repository = repository
    
    def handle(self, query: GetAccountBalanceQuery) -> AccountBalanceDTO:
        """
        Traiter GetAccountBalanceQuery.
        
        Args:
            query: Query
            
        Returns:
            DTO avec solde
            
        Raises:
            ValueError: Si compte introuvable
        """
        account_number = AccountNumber(query.account_number)
        account = self.repository.get_by_account_number(account_number)
        
        if not account:
            raise ValueError(f"Account {account_number} not found")
        
        return AccountBalanceDTO(
            account_number=str(account.account_number),
            balance=account.balance.amount,
            currency=account.balance.currency,
            status=account.status.value
        )


class GetAccountHistoryHandler(QueryHandler):
    """Handler : Obtenir historique."""
    
    def __init__(self, event_store: EventStore):
        self.event_store = event_store
    
    def handle(self, query: GetAccountHistoryQuery) -> List[TransactionDTO]:
        """
        Traiter GetAccountHistoryQuery.
        
        Args:
            query: Query
            
        Returns:
            Liste de transactions
        """
        # Récupérer événements
        events = self.event_store.get_events(query.account_number)
        
        # Convertir en DTOs
        transactions = []
        running_balance = Decimal("0")
        
        for event in events:
            if isinstance(event, AccountOpenedEvent):
                running_balance = event.initial_balance.amount
                
            elif isinstance(event, MoneyDepositedEvent):
                running_balance += event.amount.amount
                
                transactions.append(TransactionDTO(
                    timestamp=event.occurred_at,
                    type="deposit",
                    amount=event.amount.amount,
                    currency=event.amount.currency,
                    description=event.description,
                    balance_after=running_balance
                ))
                
            elif isinstance(event, MoneyWithdrawnEvent):
                running_balance -= event.amount.amount
                
                transactions.append(TransactionDTO(
                    timestamp=event.occurred_at,
                    type="withdrawal",
                    amount=event.amount.amount,
                    currency=event.amount.currency,
                    description=event.description,
                    balance_after=running_balance
                ))
        
        # Filtrer par dates si spécifiées
        if query.from_date:
            transactions = [
                t for t in transactions
                if t.timestamp >= query.from_date
            ]
        
        if query.to_date:
            transactions = [
                t for t in transactions
                if t.timestamp <= query.to_date
            ]
        
        return transactions


"""
═══════════════════════════════════════════════════════════════════════════
TDD : TESTS (Test Driven Development)
═══════════════════════════════════════════════════════════════════════════

[IDEE] TDD

1. RED : Écrire test qui échoue
2. GREEN : Écrire code minimum pour passer
3. REFACTOR : Améliorer code


Tests AVANT implémentation !
"""

# ============================================================================
# TESTS UNITAIRES : Value Objects
# ============================================================================

class TestMoney:
    """Tests TDD pour Money."""
    
    def test_create_money(self):
        """Test création Money."""
        money = Money(Decimal("100.50"), "EUR")
        
        assert money.amount == Decimal("100.50")
        assert money.currency == "EUR"
    
    def test_money_addition(self):
        """Test addition."""
        m1 = Money(Decimal("100"), "EUR")
        m2 = Money(Decimal("50"), "EUR")
        
        result = m1 + m2
        
        assert result.amount == Decimal("150")
        assert result.currency == "EUR"
    
    def test_money_addition_different_currencies_raises_error(self):
        """Test addition devises différentes."""
        m1 = Money(Decimal("100"), "EUR")
        m2 = Money(Decimal("50"), "USD")
        
        with pytest.raises(ValueError, match="Cannot add"):
            m1 + m2
    
    def test_money_subtraction(self):
        """Test soustraction."""
        m1 = Money(Decimal("100"), "EUR")
        m2 = Money(Decimal("30"), "EUR")
        
        result = m1 - m2
        
        assert result.amount == Decimal("70")
    
    def test_money_comparison(self):
        """Test comparaisons."""
        m1 = Money(Decimal("100"), "EUR")
        m2 = Money(Decimal("50"), "EUR")
        
        assert m1 > m2
        assert m2 < m1
        assert m1 >= m2
        assert m2 <= m1
    
    def test_money_is_positive(self):
        """Test is_positive."""
        assert Money(Decimal("100"), "EUR").is_positive()
        assert not Money(Decimal("-100"), "EUR").is_positive()
        assert not Money(Decimal("0"), "EUR").is_positive()
    
    def test_money_zero(self):
        """Test factory zero."""
        zero = Money.zero("EUR")
        
        assert zero.amount == Decimal("0")
        assert zero.is_zero()


# ============================================================================
# TESTS UNITAIRES : Account Aggregate
# ============================================================================

class TestAccount:
    """Tests TDD pour Account (Event Sourcing)."""
    
    def test_open_account(self):
        """Test ouverture compte."""
        account_number = AccountNumber.generate()
        customer_id = CustomerId.generate()
        
        account = Account.open(
            account_number=account_number,
            customer_id=customer_id,
            account_type=AccountType.CHECKING,
            initial_deposit=Money(Decimal("100"), "EUR")
        )
        
        assert account.account_number == account_number
        assert account.customer_id == customer_id
        assert account.balance == Money(Decimal("100"), "EUR")
        assert account.is_active
        assert account.version == 1
    
    def test_deposit_increases_balance(self):
        """Test dépôt augmente solde."""
        account = Account.open(
            AccountNumber.generate(),
            CustomerId.generate(),
            AccountType.CHECKING,
            Money.zero("EUR")
        )
        
        account.deposit(Money(Decimal("50"), "EUR"))
        
        assert account.balance == Money(Decimal("50"), "EUR")
        assert account.version == 2  # Open + Deposit
    
    def test_withdraw_decreases_balance(self):
        """Test retrait diminue solde."""
        account = Account.open(
            AccountNumber.generate(),
            CustomerId.generate(),
            AccountType.CHECKING,
            Money(Decimal("100"), "EUR")
        )
        
        account.withdraw(Money(Decimal("30"), "EUR"))
        
        assert account.balance == Money(Decimal("70"), "EUR")
    
    def test_withdraw_insufficient_funds_raises_error(self):
        """Test retrait solde insuffisant."""
        account = Account.open(
            AccountNumber.generate(),
            CustomerId.generate(),
            AccountType.CHECKING,
            Money(Decimal("50"), "EUR")
        )
        
        with pytest.raises(ValueError, match="Insufficient funds"):
            account.withdraw(Money(Decimal("100"), "EUR"))
    
    def test_freeze_account(self):
        """Test gel compte."""
        account = Account.open(
            AccountNumber.generate(),
            CustomerId.generate(),
            AccountType.CHECKING
        )
        
        account.freeze("Suspicious activity")
        
        assert account.is_frozen
    
    def test_frozen_account_cannot_withdraw(self):
        """Test compte gelé ne peut pas retirer."""
        account = Account.open(
            AccountNumber.generate(),
            CustomerId.generate(),
            AccountType.CHECKING,
            Money(Decimal("100"), "EUR")
        )
        
        account.freeze("Test")
        
        with pytest.raises(ValueError, match="frozen"):
            account.withdraw(Money(Decimal("10"), "EUR"))
    
    def test_close_account_with_zero_balance(self):
        """Test fermeture compte avec solde zéro."""
        account = Account.open(
            AccountNumber.generate(),
            CustomerId.generate(),
            AccountType.CHECKING
        )
        
        account.close("Customer request")
        
        assert account.is_closed
    
    def test_close_account_with_balance_raises_error(self):
        """Test fermeture avec solde non nul échoue."""
        account = Account.open(
            AccountNumber.generate(),
            CustomerId.generate(),
            AccountType.CHECKING,
            Money(Decimal("100"), "EUR")
        )
        
        with pytest.raises(ValueError, match="non-zero balance"):
            account.close()
    
    def test_event_sourcing_reconstruction(self):
        """
        Test reconstruction depuis événements.
        
        [IDEE] TDD : Event Sourcing
        """
        account_number = AccountNumber.generate()
        customer_id = CustomerId.generate()
        
        # Créer événements
        events = [
            AccountOpenedEvent(
                account_number=account_number,
                customer_id=customer_id,
                initial_balance=Money(Decimal("100"), "EUR"),
                account_type="checking"
            ),
            MoneyDepositedEvent(
                account_number=account_number,
                amount=Money(Decimal("50"), "EUR")
            ),
            MoneyWithdrawnEvent(
                account_number=account_number,
                amount=Money(Decimal("30"), "EUR")
            )
        ]
        
        # Reconstruire compte depuis événements
        account = Account.from_events(account_number, events)
        
        # Vérifier état reconstruit
        assert account.balance == Money(Decimal("120"), "EUR")  # 100 + 50 - 30
        assert account.version == 3
        assert account.is_active


# Ce fichier continue...
# ============================================================================

# ============================================================================
# [BANQUE] PROJET PRATIQUE 2 : SYSTÈME BANCAIRE (PARTIE 3)
# ============================================================================
#
# Suite du projet : Infrastructure (Adapters)
#
# [OBJECTIF] CONTENU :
# - Adapters (implémentations des Ports)
# - InMemoryEventStore (Event Sourcing)
# - InMemoryAccountRepository
# - EventPublisher
# - Unit of Work Pattern
# - Tests d'intégration
#
# ============================================================================


# Suite des Parties 1 et 2...

from typing import List, Dict, Optional, Set
from datetime import datetime
from collections import defaultdict
import threading
import copy


"""
═══════════════════════════════════════════════════════════════════════════
INFRASTRUCTURE : ADAPTERS (implémentations des Ports)
═══════════════════════════════════════════════════════════════════════════

[IDEE] ADAPTERS

Implémentent les Ports (interfaces).
Détails techniques (DB, fichiers, APIs).
Interchangeables sans toucher au domaine.
"""


# ============================================================================
# ADAPTER : Event Store (In-Memory)
# ============================================================================

class ConcurrencyException(Exception):
    """Exception levée lors de conflit de concurrence."""
    pass


class InMemoryEventStore:
    """
    [IDEE] ADAPTER : Event Store en mémoire
    
    Implémente EventStore Protocol.
    Stocke événements par aggregate.
    Gère optimistic locking (versions).
    
    Format stockage :
    {
        "aggregate_id": [
            (version, event),
            (version, event),
            ...
        ]
    }
    """
    
    def __init__(self):
        """Initialise event store vide."""
        # Stockage : aggregate_id -> [(version, event)]
        self._events: Dict[str, List[tuple[int, DomainEvent]]] = defaultdict(list)
        
        # Lock pour thread-safety
        self._lock = threading.Lock()
    
    def save_events(
        self,
        aggregate_id: str,
        events: List[DomainEvent],
        expected_version: int
    ) -> None:
        """
        Sauvegarder événements avec optimistic locking.
        
        Args:
            aggregate_id: ID de l'aggregate
            events: Événements à sauvegarder
            expected_version: Version attendue
            
        Raises:
            ConcurrencyException: Si version ne correspond pas
        """
        with self._lock:
            # Vérifier version actuelle
            current_version = self._get_current_version(aggregate_id)
            
            if current_version != expected_version:
                raise ConcurrencyException(
                    f"Concurrency conflict for {aggregate_id}. "
                    f"Expected version {expected_version}, "
                    f"but current is {current_version}"
                )
            
            # Ajouter événements avec versions incrémentales
            for i, event in enumerate(events):
                version = expected_version + i + 1
                self._events[aggregate_id].append((version, event))
    
    def get_events(
        self,
        aggregate_id: str,
        from_version: int = 0
    ) -> List[DomainEvent]:
        """
        Récupérer événements d'un aggregate.
        
        Args:
            aggregate_id: ID de l'aggregate
            from_version: Version de départ
            
        Returns:
            Liste d'événements (ordre chronologique)
        """
        with self._lock:
            all_events = self._events.get(aggregate_id, [])
            
            # Filtrer par version et extraire événements
            return [
                event for version, event in all_events
                if version > from_version
            ]
    
    def _get_current_version(self, aggregate_id: str) -> int:
        """
        Obtenir version actuelle d'un aggregate.
        
        Args:
            aggregate_id: ID de l'aggregate
            
        Returns:
            Version actuelle (0 si aucun événement)
        """
        events = self._events.get(aggregate_id, [])
        if not events:
            return 0
        
        # Dernière version
        return events[-1][0]
    
    def get_all_aggregates(self) -> List[str]:
        """Obtenir liste de tous les aggregate IDs."""
        return list(self._events.keys())
    
    def clear(self) -> None:
        """Vider event store (pour tests)."""
        with self._lock:
            self._events.clear()


# ============================================================================
# ADAPTER : Account Repository (In-Memory avec Event Sourcing)
# ============================================================================

class InMemoryAccountRepository:
    """
    [IDEE] ADAPTER : Repository en mémoire
    
    Implémente AccountRepository Protocol.
    Utilise Event Store pour persistance.
    """
    
    def __init__(self, event_store: InMemoryEventStore):
        """
        Initialise repository.
        
        Args:
            event_store: Event store pour persistance
        """
        self.event_store = event_store
        
        # Cache optionnel (performance)
        self._cache: Dict[str, Account] = {}
        self._lock = threading.Lock()
    
    def save(self, account: Account) -> None:
        """
        Sauvegarder compte.
        
        Args:
            account: Compte à sauvegarder
            
        Raises:
            ConcurrencyException: Si conflit de version
        """
        # Récupérer événements non commitées
        uncommitted_events = account.get_uncommitted_events()
        
        if not uncommitted_events:
            return  # Rien à sauvegarder
        
        # Sauvegarder événements
        aggregate_id = str(account.account_number)
        expected_version = account.version - len(uncommitted_events)
        
        self.event_store.save_events(
            aggregate_id,
            uncommitted_events,
            expected_version
        )
        
        # Marquer événements comme commitées
        account.mark_events_as_committed()
        
        # Mettre à jour cache
        with self._lock:
            self._cache[aggregate_id] = account
    
    def get_by_account_number(
        self,
        account_number: AccountNumber
    ) -> Optional[Account]:
        """
        Récupérer compte par numéro.
        
        Args:
            account_number: Numéro de compte
            
        Returns:
            Compte si trouvé, None sinon
        """
        aggregate_id = str(account_number)
        
        # Vérifier cache d'abord
        with self._lock:
            if aggregate_id in self._cache:
                return self._cache[aggregate_id]
        
        # Récupérer événements
        events = self.event_store.get_events(aggregate_id)
        
        if not events:
            return None
        
        # Reconstruire compte depuis événements
        account = Account.from_events(account_number, events)
        
        # Mettre en cache
        with self._lock:
            self._cache[aggregate_id] = account
        
        return account
    
    def exists(self, account_number: AccountNumber) -> bool:
        """
        Vérifier si compte existe.
        
        Args:
            account_number: Numéro de compte
            
        Returns:
            True si existe
        """
        aggregate_id = str(account_number)
        
        # Vérifier cache
        with self._lock:
            if aggregate_id in self._cache:
                return True
        
        # Vérifier event store
        events = self.event_store.get_events(aggregate_id)
        return len(events) > 0
    
    def get_all(self) -> List[Account]:
        """
        Récupérer tous les comptes.
        
        Returns:
            Liste de tous les comptes
        """
        accounts = []
        
        # Récupérer tous les aggregate IDs
        aggregate_ids = self.event_store.get_all_aggregates()
        
        for aggregate_id in aggregate_ids:
            account_number = AccountNumber(aggregate_id)
            account = self.get_by_account_number(account_number)
            
            if account:
                accounts.append(account)
        
        return accounts
    
    def clear_cache(self) -> None:
        """Vider cache (pour tests)."""
        with self._lock:
            self._cache.clear()


# ============================================================================
# ADAPTER : Event Publisher (In-Memory)
# ============================================================================

class InMemoryEventPublisher:
    """
    [IDEE] ADAPTER : Event Publisher en mémoire
    
    Implémente EventPublisher Protocol.
    Notifie subscribers des événements.
    """
    
    def __init__(self):
        """Initialise publisher vide."""
        # Subscribers : event_type -> [handlers]
        self._subscribers: Dict[str, List[callable]] = defaultdict(list)
        
        # Historique des événements publiés
        self._published_events: List[DomainEvent] = []
        
        self._lock = threading.Lock()
    
    def subscribe(
        self,
        event_type: str,
        handler: callable
    ) -> None:
        """
        S'abonner à un type d'événement.
        
        Args:
            event_type: Type d'événement (ex: "AccountOpened")
            handler: Fonction appelée lors de l'événement
        """
        with self._lock:
            self._subscribers[event_type].append(handler)
    
    def publish(self, event: DomainEvent) -> None:
        """
        Publier événement.
        
        Args:
            event: Événement à publier
        """
        with self._lock:
            # Enregistrer dans historique
            self._published_events.append(event)
            
            # Notifier subscribers
            event_type = event.event_type()
            handlers = self._subscribers.get(event_type, [])
            
            for handler in handlers:
                try:
                    handler(event)
                except Exception as e:
                    # En production : logger l'erreur
                    print(f"Error in event handler: {e}")
    
    def get_published_events(self) -> List[DomainEvent]:
        """Obtenir historique événements publiés."""
        with self._lock:
            return self._published_events.copy()
    
    def clear(self) -> None:
        """Vider historique (pour tests)."""
        with self._lock:
            self._published_events.clear()


# ============================================================================
# PATTERN : Unit of Work
# ============================================================================

class UnitOfWork:
    """
    [IDEE] PATTERN UNIT OF WORK
    
    Gère transactions et coordination entre repositories.
    Garantit atomicité des opérations.
    """
    
    def __init__(
        self,
        account_repository: InMemoryAccountRepository,
        event_publisher: InMemoryEventPublisher
    ):
        """
        Initialise Unit of Work.
        
        Args:
            account_repository: Repository comptes
            event_publisher: Publisher événements
        """
        self.account_repository = account_repository
        self.event_publisher = event_publisher
        
        # Aggregates modifiés dans cette transaction
        self._tracked_accounts: Set[Account] = set()
    
    def register_account(self, account: Account) -> None:
        """
        Enregistrer compte pour suivi.
        
        Args:
            account: Compte à suivre
        """
        self._tracked_accounts.add(account)
    
    def commit(self) -> None:
        """
        Committer transaction.
        
        Sauvegarde tous les aggregates et publie événements.
        
        Raises:
            Exception: Si sauvegarde échoue
        """
        try:
            # Sauvegarder tous les comptes suivis
            for account in self._tracked_accounts:
                # Sauvegarder
                self.account_repository.save(account)
                
                # Publier événements
                for event in account.get_uncommitted_events():
                    self.event_publisher.publish(event)
                
                # Marquer comme commitées
                account.mark_events_as_committed()
            
            # Nettoyer suivi
            self._tracked_accounts.clear()
            
        except Exception as e:
            # Rollback : en production, annuler changements
            self._tracked_accounts.clear()
            raise e
    
    def rollback(self) -> None:
        """Annuler transaction."""
        self._tracked_accounts.clear()


# ============================================================================
# TESTS D'INTÉGRATION
# ============================================================================

class TestInfrastructureIntegration:
    """
    Tests d'intégration : Infrastructure complète.
    
    Teste interactions entre tous les adapters.
    """
    
    @pytest.fixture
    def event_store(self):
        """Fixture : Event store."""
        store = InMemoryEventStore()
        yield store
        store.clear()
    
    @pytest.fixture
    def repository(self, event_store):
        """Fixture : Repository."""
        return InMemoryAccountRepository(event_store)
    
    @pytest.fixture
    def publisher(self):
        """Fixture : Event publisher."""
        pub = InMemoryEventPublisher()
        yield pub
        pub.clear()
    
    def test_save_and_load_account(self, repository):
        """
        Test sauvegarde et chargement compte.
        
        [IDEE] INTÉGRATION : Repository + EventStore
        """
        # Créer compte
        account_number = AccountNumber.generate()
        account = Account.open(
            account_number,
            CustomerId.generate(),
            AccountType.CHECKING,
            Money(Decimal("1000"), "EUR")
        )
        
        # Sauvegarder
        repository.save(account)
        
        # Recharger
        loaded_account = repository.get_by_account_number(account_number)
        
        # Vérifier
        assert loaded_account is not None
        assert loaded_account.account_number == account_number
        assert loaded_account.balance == Money(Decimal("1000"), "EUR")
    
    def test_event_sourcing_reconstruction(self, repository):
        """
        Test reconstruction via Event Sourcing.
        
        [IDEE] INTÉGRATION : Repository + EventStore + Account
        """
        # Créer compte
        account_number = AccountNumber.generate()
        account = Account.open(
            account_number,
            CustomerId.generate(),
            AccountType.CHECKING,
            Money(Decimal("500"), "EUR")
        )
        
        # Opérations
        account.deposit(Money(Decimal("200"), "EUR"))
        account.withdraw(Money(Decimal("100"), "EUR"))
        
        # Sauvegarder
        repository.save(account)
        
        # Vider cache pour forcer reconstruction
        repository.clear_cache()
        
        # Recharger (reconstruction depuis événements)
        loaded_account = repository.get_by_account_number(account_number)
        
        # Vérifier état reconstruit
        assert loaded_account.balance == Money(Decimal("600"), "EUR")  # 500+200-100
        assert loaded_account.version == 3  # Open + Deposit + Withdraw
    
    def test_optimistic_locking(self, event_store):
        """
        Test optimistic locking (concurrence).
        
        [IDEE] INTÉGRATION : EventStore concurrence
        """
        aggregate_id = "test-account"
        
        # Sauvegarder événements (version 0 -> 1)
        event1 = AccountOpenedEvent(
            account_number=AccountNumber(aggregate_id),
            customer_id=CustomerId.generate(),
            initial_balance=Money.zero(),
            account_type="checking"
        )
        
        event_store.save_events(aggregate_id, [event1], expected_version=0)
        
        # Tentative de sauvegarde avec mauvaise version
        event2 = MoneyDepositedEvent(
            account_number=AccountNumber(aggregate_id),
            amount=Money(Decimal("100"), "EUR")
        )
        
        with pytest.raises(ConcurrencyException):
            # Version attendue 0, mais actuelle est 1
            event_store.save_events(aggregate_id, [event2], expected_version=0)
    
    def test_event_publisher_notifies_subscribers(self, publisher):
        """
        Test publication événements.
        
        [IDEE] INTÉGRATION : EventPublisher + Subscribers
        """
        # Compteurs pour vérifier appels
        calls = {"account_opened": 0, "money_deposited": 0}
        
        def on_account_opened(event):
            calls["account_opened"] += 1
        
        def on_money_deposited(event):
            calls["money_deposited"] += 1
        
        # S'abonner
        publisher.subscribe("AccountOpened", on_account_opened)
        publisher.subscribe("MoneyDeposited", on_money_deposited)
        
        # Publier événements
        publisher.publish(AccountOpenedEvent(
            account_number=AccountNumber.generate(),
            customer_id=CustomerId.generate(),
            initial_balance=Money.zero(),
            account_type="checking"
        ))
        
        publisher.publish(MoneyDepositedEvent(
            account_number=AccountNumber.generate(),
            amount=Money(Decimal("100"), "EUR")
        ))
        
        # Vérifier notifications
        assert calls["account_opened"] == 1
        assert calls["money_deposited"] == 1
    
    def test_unit_of_work_commits_all(self, repository, publisher):
        """
        Test Unit of Work.
        
        [IDEE] INTÉGRATION : UnitOfWork + Repository + Publisher
        """
        # Créer Unit of Work
        uow = UnitOfWork(repository, publisher)
        
        # Créer comptes
        account1 = Account.open(
            AccountNumber.generate(),
            CustomerId.generate(),
            AccountType.CHECKING,
            Money(Decimal("100"), "EUR")
        )
        
        account2 = Account.open(
            AccountNumber.generate(),
            CustomerId.generate(),
            AccountType.SAVINGS,
            Money(Decimal("200"), "EUR")
        )
        
        # Enregistrer pour suivi
        uow.register_account(account1)
        uow.register_account(account2)
        
        # Commit
        uow.commit()
        
        # Vérifier que tout a été sauvegardé
        loaded1 = repository.get_by_account_number(account1.account_number)
        loaded2 = repository.get_by_account_number(account2.account_number)
        
        assert loaded1 is not None
        assert loaded2 is not None
        
        # Vérifier événements publiés
        published = publisher.get_published_events()
        assert len(published) == 2  # 2 AccountOpened


# ============================================================================
# SERVICE APPLICATIF COMPLET
# ============================================================================

class BankingService:
    """
    [IDEE] APPLICATION SERVICE
    
    Façade pour toutes les opérations bancaires.
    Utilise CQRS (Commands + Queries).
    """
    
    def __init__(
        self,
        repository: InMemoryAccountRepository,
        publisher: InMemoryEventPublisher,
        transfer_service: TransferService
    ):
        """
        Initialise service bancaire.
        
        Args:
            repository: Repository comptes
            publisher: Publisher événements
            transfer_service: Service virements
        """
        self.repository = repository
        self.publisher = publisher
        self.transfer_service = transfer_service
        
        # Créer handlers
        self.open_account_handler = OpenAccountHandler(repository, publisher)
        self.deposit_handler = DepositMoneyHandler(repository, publisher)
        self.withdraw_handler = WithdrawMoneyHandler(repository, publisher)
        self.transfer_handler = TransferMoneyHandler(transfer_service, publisher)
        
        self.balance_query = GetAccountBalanceHandler(repository)
        self.history_query = GetAccountHistoryHandler(
            repository.event_store
        )
    
    # ========================================================================
    # COMMANDS
    # ========================================================================
    
    def open_account(
        self,
        customer_id: str,
        account_type: str,
        initial_deposit: Optional[Decimal] = None
    ) -> AccountNumber:
        """
        Ouvrir nouveau compte.
        
        Args:
            customer_id: ID client
            account_type: Type ("checking" ou "savings")
            initial_deposit: Dépôt initial
            
        Returns:
            Numéro de compte créé
        """
        command = OpenAccountCommand(
            customer_id=customer_id,
            account_type=account_type,
            initial_deposit=initial_deposit
        )
        
        return self.open_account_handler.handle(command)
    
    def deposit(
        self,
        account_number: str,
        amount: Decimal,
        description: str = ""
    ) -> None:
        """Déposer argent."""
        command = DepositMoneyCommand(
            account_number=account_number,
            amount=amount,
            description=description
        )
        
        self.deposit_handler.handle(command)
    
    def withdraw(
        self,
        account_number: str,
        amount: Decimal,
        description: str = ""
    ) -> None:
        """Retirer argent."""
        command = WithdrawMoneyCommand(
            account_number=account_number,
            amount=amount,
            description=description
        )
        
        self.withdraw_handler.handle(command)
    
    def transfer(
        self,
        from_account: str,
        to_account: str,
        amount: Decimal,
        description: str = ""
    ) -> None:
        """Virer argent."""
        command = TransferMoneyCommand(
            from_account_number=from_account,
            to_account_number=to_account,
            amount=amount,
            description=description
        )
        
        self.transfer_handler.handle(command)
    
    # ========================================================================
    # QUERIES
    # ========================================================================
    
    def get_balance(self, account_number: str) -> AccountBalanceDTO:
        """Obtenir solde."""
        query = GetAccountBalanceQuery(account_number=account_number)
        return self.balance_query.handle(query)
    
    def get_history(
        self,
        account_number: str,
        from_date: Optional[datetime] = None,
        to_date: Optional[datetime] = None
    ) -> List[TransactionDTO]:
        """Obtenir historique."""
        query = GetAccountHistoryQuery(
            account_number=account_number,
            from_date=from_date,
            to_date=to_date
        )
        
        return self.history_query.handle(query)
    
    def get_account(self, account_number: str) -> Optional[Account]:
        """Obtenir compte complet."""
        return self.repository.get_by_account_number(
            AccountNumber(account_number)
        )


# ============================================================================
# FACTORY : Application Setup
# ============================================================================

def create_banking_application() -> BankingService:
    """
    [IDEE] FACTORY : Créer application complète
    
    Assemble tous les composants (DI container simple).
    
    Returns:
        Service bancaire configuré
    """
    # Infrastructure
    event_store = InMemoryEventStore()
    repository = InMemoryAccountRepository(event_store)
    publisher = InMemoryEventPublisher()
    
    # Domain services
    transfer_service = TransferService(repository)
    
    # Application service
    banking_service = BankingService(
        repository,
        publisher,
        transfer_service
    )
    
    return banking_service


# Ce fichier continue avec la Partie 4 (Application complète)...
# ============================================================================

# ============================================================================
# [BANQUE] PROJET PRATIQUE 2 : SYSTÈME BANCAIRE (PARTIE 4 - FINALE)
# ============================================================================
#
# Fin du projet : Application complète et démonstrations
#
# [OBJECTIF] CONTENU :
# - CLI (Command Line Interface)
# - Démonstration complète Event Sourcing
# - Tests acceptance (end-to-end)
# - Comparaison avec/sans Event Sourcing
# - Récapitulatif complet du projet
#
# ============================================================================


# Suite de la Partie 3...

from typing import List, Optional
from decimal import Decimal
from datetime import datetime
import sys


"""
═══════════════════════════════════════════════════════════════════════════
CLI : INTERFACE LIGNE DE COMMANDE
═══════════════════════════════════════════════════════════════════════════
"""

class BankingCLI:
    """
    [IDEE] CLI : Interface utilisateur bancaire
    
    Permet interactions avec système bancaire via terminal.
    """
    
    def __init__(self, banking_service: BankingService):
        """
        Initialise CLI.
        
        Args:
            banking_service: Service bancaire
        """
        self.service = banking_service
        self.running = True
    
    def start(self) -> None:
        """Démarrer CLI."""
        print("\n" + "="*60)
        print("[BANQUE] BANKING SYSTEM - Event Sourcing Demo")
        print("="*60)
        
        while self.running:
            self.show_menu()
            choice = input("\nYour choice: ").strip()
            
            try:
                self.handle_choice(choice)
            except Exception as e:
                print(f"\n[X] Error: {e}")
    
    def show_menu(self) -> None:
        """Afficher menu."""
        print("\n" + "-"*60)
        print("MENU:")
        print("  1. Open new account")
        print("  2. Deposit money")
        print("  3. Withdraw money")
        print("  4. Transfer money")
        print("  5. Check balance")
        print("  6. View transaction history")
        print("  7. List all accounts")
        print("  8. View event store (Event Sourcing)")
        print("  0. Exit")
        print("-"*60)
    
    def handle_choice(self, choice: str) -> None:
        """Traiter choix utilisateur."""
        handlers = {
            "1": self.open_account,
            "2": self.deposit,
            "3": self.withdraw,
            "4": self.transfer,
            "5": self.check_balance,
            "6": self.view_history,
            "7": self.list_accounts,
            "8": self.view_event_store,
            "0": self.exit
        }
        
        handler = handlers.get(choice)
        if handler:
            handler()
        else:
            print("Invalid choice")
    
    def open_account(self) -> None:
        """Ouvrir nouveau compte."""
        print("\n--- OPEN NEW ACCOUNT ---")
        
        customer_id = input("Customer ID: ").strip()
        
        print("Account types: 1=Checking, 2=Savings")
        account_type_choice = input("Type: ").strip()
        account_type = "checking" if account_type_choice == "1" else "savings"
        
        initial = input("Initial deposit (or Enter for 0): ").strip()
        initial_deposit = Decimal(initial) if initial else None
        
        try:
            account_number = self.service.open_account(
                customer_id=customer_id,
                account_type=account_type,
                initial_deposit=initial_deposit
            )
            
            print(f"\n[OK] Account opened successfully!")
            print(f"Account number: {account_number}")
            
        except Exception as e:
            print(f"\n[X] Failed to open account: {e}")
    
    def deposit(self) -> None:
        """Déposer argent."""
        print("\n--- DEPOSIT MONEY ---")
        
        account_number = input("Account number: ").strip()
        amount = Decimal(input("Amount: ").strip())
        description = input("Description (optional): ").strip()
        
        try:
            self.service.deposit(account_number, amount, description)
            print(f"\n[OK] Deposited {amount} EUR successfully!")
            
            # Afficher nouveau solde
            balance = self.service.get_balance(account_number)
            print(f"New balance: {balance.balance} {balance.currency}")
            
        except Exception as e:
            print(f"\n[X] Deposit failed: {e}")
    
    def withdraw(self) -> None:
        """Retirer argent."""
        print("\n--- WITHDRAW MONEY ---")
        
        account_number = input("Account number: ").strip()
        amount = Decimal(input("Amount: ").strip())
        description = input("Description (optional): ").strip()
        
        try:
            self.service.withdraw(account_number, amount, description)
            print(f"\n[OK] Withdrew {amount} EUR successfully!")
            
            # Afficher nouveau solde
            balance = self.service.get_balance(account_number)
            print(f"New balance: {balance.balance} {balance.currency}")
            
        except Exception as e:
            print(f"\n[X] Withdrawal failed: {e}")
    
    def transfer(self) -> None:
        """Virer argent."""
        print("\n--- TRANSFER MONEY ---")
        
        from_account = input("From account: ").strip()
        to_account = input("To account: ").strip()
        amount = Decimal(input("Amount: ").strip())
        description = input("Description (optional): ").strip()
        
        try:
            self.service.transfer(from_account, to_account, amount, description)
            print(f"\n[OK] Transferred {amount} EUR successfully!")
            
        except Exception as e:
            print(f"\n[X] Transfer failed: {e}")
    
    def check_balance(self) -> None:
        """Vérifier solde."""
        print("\n--- CHECK BALANCE ---")
        
        account_number = input("Account number: ").strip()
        
        try:
            balance = self.service.get_balance(account_number)
            
            print(f"\nAccount: {balance.account_number}")
            print(f"Balance: {balance.balance} {balance.currency}")
            print(f"Status: {balance.status}")
            
        except Exception as e:
            print(f"\n[X] Failed to get balance: {e}")
    
    def view_history(self) -> None:
        """Voir historique."""
        print("\n--- TRANSACTION HISTORY ---")
        
        account_number = input("Account number: ").strip()
        
        try:
            history = self.service.get_history(account_number)
            
            if not history:
                print("\nNo transactions yet")
                return
            
            print(f"\nTransactions for account {account_number}:")
            print("-" * 80)
            print(f"{'Date':<20} {'Type':<12} {'Amount':>12} {'Balance':>12} {'Description':<20}")
            print("-" * 80)
            
            for tx in history:
                date_str = tx.timestamp.strftime("%Y-%m-%d %H:%M:%S")
                amount_str = f"{tx.amount:,.2f} {tx.currency}"
                balance_str = f"{tx.balance_after:,.2f}"
                
                print(f"{date_str:<20} {tx.type:<12} {amount_str:>12} {balance_str:>12} {tx.description:<20}")
            
            print("-" * 80)
            
        except Exception as e:
            print(f"\n[X] Failed to get history: {e}")
    
    def list_accounts(self) -> None:
        """Lister tous les comptes."""
        print("\n--- ALL ACCOUNTS ---")
        
        accounts = self.service.repository.get_all()
        
        if not accounts:
            print("\nNo accounts yet")
            return
        
        print(f"\nTotal accounts: {len(accounts)}")
        print("-" * 80)
        print(f"{'Account Number':<20} {'Type':<12} {'Balance':>15} {'Status':<10}")
        print("-" * 80)
        
        for account in accounts:
            acc_type = account.account_type.value if account.account_type else "N/A"
            balance = f"{account.balance.amount:,.2f} {account.balance.currency}"
            
            print(f"{str(account.account_number):<20} {acc_type:<12} {balance:>15} {account.status.value:<10}")
        
        print("-" * 80)
    
    def view_event_store(self) -> None:
        """
        [IDEE] EVENT SOURCING : Voir événements bruts
        
        Montre tous les événements stockés.
        """
        print("\n--- EVENT STORE (Event Sourcing) ---")
        
        event_store = self.service.repository.event_store
        aggregate_ids = event_store.get_all_aggregates()
        
        if not aggregate_ids:
            print("\nNo events stored yet")
            return
        
        for aggregate_id in aggregate_ids:
            print(f"\n[PACKAGE] Aggregate: {aggregate_id}")
            print("-" * 80)
            
            events = event_store.get_events(aggregate_id)
            
            for i, event in enumerate(events, 1):
                print(f"\nEvent #{i} - {event.event_type()}")
                print(f"  Timestamp: {event.occurred_at}")
                print(f"  Event ID: {event.event_id}")
                
                # Afficher données spécifiques
                if isinstance(event, AccountOpenedEvent):
                    print(f"  Customer: {event.customer_id}")
                    print(f"  Type: {event.account_type}")
                    print(f"  Initial balance: {event.initial_balance}")
                    
                elif isinstance(event, MoneyDepositedEvent):
                    print(f"  Amount: {event.amount}")
                    print(f"  Description: {event.description}")
                    
                elif isinstance(event, MoneyWithdrawnEvent):
                    print(f"  Amount: {event.amount}")
                    print(f"  Description: {event.description}")
        
        print("\n" + "="*80)
    
    def exit(self) -> None:
        """Quitter."""
        print("\n[WAVING_HAND_SIGN] Goodbye!")
        self.running = False


"""
═══════════════════════════════════════════════════════════════════════════
DÉMONSTRATION COMPLÈTE EVENT SOURCING
═══════════════════════════════════════════════════════════════════════════
"""

def demo_event_sourcing():
    """
    [IDEE] DÉMONSTRATION : Event Sourcing en action
    
    Montre :
    - Création événements
    - Reconstruction depuis événements
    - Time travel (état à n'importe quel moment)
    - Audit trail complet
    """
    print("\n" + "="*80)
    print("[DEMARRAGE] EVENT SOURCING DEMONSTRATION")
    print("="*80)
    
    # Créer application
    app = create_banking_application()
    
    # ========================================================================
    print("\n[NOTE] Step 1: Opening account...")
    # ========================================================================
    
    account_number = app.open_account(
        customer_id="CUST-001",
        account_type="checking",
        initial_deposit=Decimal("1000")
    )
    
    print(f"[OK] Account opened: {account_number}")
    print(f"   Initial balance: 1000 EUR")
    
    # ========================================================================
    print("\n[NOTE] Step 2: Performing transactions...")
    # ========================================================================
    
    # Dépôt
    app.deposit(str(account_number), Decimal("500"), "Salary")
    print("[OK] Deposited 500 EUR (Salary)")
    
    # Retrait
    app.withdraw(str(account_number), Decimal("200"), "ATM withdrawal")
    print("[OK] Withdrew 200 EUR (ATM)")
    
    # Dépôt
    app.deposit(str(account_number), Decimal("150"), "Refund")
    print("[OK] Deposited 150 EUR (Refund)")
    
    # Retrait
    app.withdraw(str(account_number), Decimal("100"), "Shopping")
    print("[OK] Withdrew 100 EUR (Shopping)")
    
    # ========================================================================
    print("\n[GRAPHIQUE] Step 3: Current state...")
    # ========================================================================
    
    balance = app.get_balance(str(account_number))
    print(f"[ARGENT] Current balance: {balance.balance} {balance.currency}")
    
    expected = 1000 + 500 - 200 + 150 - 100  # = 1350
    print(f"   (Expected: {expected} EUR)")
    
    # ========================================================================
    print("\n[RECHERCHE] Step 4: Event Store (source of truth)...")
    # ========================================================================
    
    event_store = app.repository.event_store
    events = event_store.get_events(str(account_number))
    
    print(f"\n[PACKAGE] Total events stored: {len(events)}")
    print("\nEvent sequence:")
    
    for i, event in enumerate(events, 1):
        print(f"  {i}. {event.event_type():<20} @ {event.occurred_at.strftime('%H:%M:%S')}")
    
    # ========================================================================
    print("\n[TEMPS] Step 5: TIME TRAVEL (Event Sourcing magic!)...")
    # ========================================================================
    
    print("\nReconstruct account state at different points in time:")
    
    # État après chaque événement
    for i in range(1, len(events) + 1):
        events_until = events[:i]
        account = Account.from_events(account_number, events_until)
        
        event_name = events[i-1].event_type()
        print(f"  After event {i} ({event_name}): {account.balance}")
    
    # ========================================================================
    print("\n[DOC] Step 6: Complete audit trail...")
    # ========================================================================
    
    history = app.get_history(str(account_number))
    
    print(f"\nTransaction history ({len(history)} transactions):")
    print("-" * 70)
    
    for tx in history:
        sign = "+" if tx.type == "deposit" else "-"
        print(f"  {tx.timestamp.strftime('%H:%M:%S')} | "
              f"{sign}{tx.amount:>8.2f} EUR | "
              f"Balance: {tx.balance_after:>8.2f} EUR | "
              f"{tx.description}")
    
    print("-" * 70)
    
    # ========================================================================
    print("\n[OK] Step 7: Verification...")
    # ========================================================================
    
    # Vider cache et reconstruire
    app.repository.clear_cache()
    
    print("\n[SYNC] Clearing cache and reconstructing from events...")
    
    reconstructed = app.get_account(str(account_number))
    
    print(f"[OK] Reconstructed balance: {reconstructed.balance}")
    print(f"   Version: {reconstructed.version}")
    
    assert reconstructed.balance.amount == Decimal("1350")
    print("\n[OK] All verifications passed!")
    
    print("\n" + "="*80)
    print("[BRAVO] EVENT SOURCING DEMONSTRATION COMPLETE!")
    print("="*80)
    
    return app


"""
═══════════════════════════════════════════════════════════════════════════
COMPARAISON : AVEC VS SANS EVENT SOURCING
═══════════════════════════════════════════════════════════════════════════
"""

def comparison_with_without_event_sourcing():
    """
    Comparaison approche traditionnelle vs Event Sourcing.
    """
    print("\n" + "="*80)
    print("[GRAPHIQUE] COMPARISON: Traditional vs Event Sourcing")
    print("="*80)
    
    print("""
╔═══════════════════════════════════════════════════════════════════════════╗
║                   TRADITIONAL CRUD                                         ║
╠═══════════════════════════════════════════════════════════════════════════╣
║                                                                            ║
║  Database stores CURRENT STATE only:                                      ║
║                                                                            ║
║  ┌─────────────────────────────────────┐                                 ║
║  │ accounts                            │                                 ║
║  ├──────────┬──────────┬───────────────┤                                 ║
║  │ id       │ balance  │ status        │                                 ║
║  ├──────────┼──────────┼───────────────┤                                 ║
║  │ ACC-001  │ 1350.00  │ active        │  <- Only current state          ║
║  └──────────┴──────────┴───────────────┘                                 ║
║                                                                            ║
║  [X] PROBLEMS:                                                             ║
║     - No history (how did we get to 1350?)                               ║
║     - Cannot reconstruct past states                                      ║
║     - No audit trail                                                      ║
║     - Lost information (UPDATE overwrites)                               ║
║                                                                            ║
╚═══════════════════════════════════════════════════════════════════════════╝

╔═══════════════════════════════════════════════════════════════════════════╗
║                      EVENT SOURCING                                        ║
╠═══════════════════════════════════════════════════════════════════════════╣
║                                                                            ║
║  Event Store stores ALL EVENTS (immutable):                               ║
║                                                                            ║
║  ┌────────────────────────────────────────────────────────────┐          ║
║  │ event_store                                                 │          ║
║  ├───────┬────────────────────┬────────────┬───────────────────┤          ║
║  │ ver   │ event_type         │ timestamp  │ data              │          ║
║  ├───────┼────────────────────┼────────────┼───────────────────┤          ║
║  │ 1     │ AccountOpened      │ 10:00:00   │ {balance: 1000}   │          ║
║  │ 2     │ MoneyDeposited     │ 10:05:00   │ {amount: 500}     │          ║
║  │ 3     │ MoneyWithdrawn     │ 10:10:00   │ {amount: 200}     │          ║
║  │ 4     │ MoneyDeposited     │ 10:15:00   │ {amount: 150}     │          ║
║  │ 5     │ MoneyWithdrawn     │ 10:20:00   │ {amount: 100}     │          ║
║  └───────┴────────────────────┴────────────┴───────────────────┘          ║
║                                                                            ║
║  Current state = replay(events)                                           ║
║  1000 + 500 - 200 + 150 - 100 = 1350 [OK]                                  ║
║                                                                            ║
║  [OK] BENEFITS:                                                             ║
║     - Complete history (every change recorded)                           ║
║     - Time travel (reconstruct any past state)                           ║
║     - Perfect audit trail                                                 ║
║     - No information loss (append-only)                                   ║
║     - Can add projections later                                          ║
║     - Debugging heaven (replay events)                                    ║
║                                                                            ║
╚═══════════════════════════════════════════════════════════════════════════╝
    """)


"""
═══════════════════════════════════════════════════════════════════════════
TESTS ACCEPTANCE (End-to-End)
═══════════════════════════════════════════════════════════════════════════
"""

class TestBankingSystemAcceptance:
    """
    Tests acceptance : Scénarios métier complets.
    
    [IDEE] ACCEPTANCE TESTS
    
    Tests du point de vue utilisateur.
    Vérifient que système satisfait besoins métier.
    """
    
    @pytest.fixture
    def app(self):
        """Fixture : Application complète."""
        return create_banking_application()
    
    def test_complete_banking_scenario(self, app):
        """
        Test scénario bancaire complet.
        
        Scénario :
        1. Alice ouvre compte avec 1000€
        2. Bob ouvre compte avec 500€
        3. Alice dépose 300€
        4. Alice vire 200€ à Bob
        5. Bob retire 100€
        6. Vérifier soldes finaux
        """
        # 1. Alice ouvre compte
        alice_account = app.open_account(
            customer_id="ALICE",
            account_type="checking",
            initial_deposit=Decimal("1000")
        )
        
        # 2. Bob ouvre compte
        bob_account = app.open_account(
            customer_id="BOB",
            account_type="checking",
            initial_deposit=Decimal("500")
        )
        
        # 3. Alice dépose 300€
        app.deposit(str(alice_account), Decimal("300"), "Salary")
        
        # 4. Alice vire 200€ à Bob
        app.transfer(
            str(alice_account),
            str(bob_account),
            Decimal("200"),
            "Gift"
        )
        
        # 5. Bob retire 100€
        app.withdraw(str(bob_account), Decimal("100"), "Cash")
        
        # 6. Vérifier soldes finaux
        alice_balance = app.get_balance(str(alice_account))
        bob_balance = app.get_balance(str(bob_account))
        
        # Alice : 1000 + 300 - 200 = 1100
        assert alice_balance.balance == Decimal("1100")
        
        # Bob : 500 + 200 - 100 = 600
        assert bob_balance.balance == Decimal("600")
    
    def test_insufficient_funds_prevents_withdrawal(self, app):
        """Test retrait impossible si solde insuffisant."""
        # Ouvrir compte avec 100€
        account = app.open_account(
            customer_id="TEST",
            account_type="checking",
            initial_deposit=Decimal("100")
        )
        
        # Tenter retrait de 200€
        with pytest.raises(ValueError, match="Insufficient funds"):
            app.withdraw(str(account), Decimal("200"))
        
        # Solde inchangé
        balance = app.get_balance(str(account))
        assert balance.balance == Decimal("100")
    
    def test_event_sourcing_reconstruction(self, app):
        """
        Test reconstruction Event Sourcing.
        
        Vérifie que compte peut être reconstruit depuis événements.
        """
        # Créer compte et faire opérations
        account_number = app.open_account(
            customer_id="TEST",
            account_type="checking",
            initial_deposit=Decimal("500")
        )
        
        app.deposit(str(account_number), Decimal("200"))
        app.withdraw(str(account_number), Decimal("100"))
        
        # Obtenir événements
        events = app.repository.event_store.get_events(str(account_number))
        
        # Reconstruire depuis événements
        reconstructed = Account.from_events(account_number, events)
        
        # Vérifier état identique
        original = app.get_account(str(account_number))
        
        assert reconstructed.balance == original.balance
        assert reconstructed.version == original.version
        assert reconstructed.status == original.status


"""
═══════════════════════════════════════════════════════════════════════════
RÉCAPITULATIF PROJET SYSTÈME BANCAIRE
═══════════════════════════════════════════════════════════════════════════
"""

BANKING_SUMMARY = """
╔═══════════════════════════════════════════════════════════════════════════╗
║            [BANQUE] BANKING SYSTEM PROJECT - COMPLETE SUMMARY                   ║
╠═══════════════════════════════════════════════════════════════════════════╣
║                                                                            ║
║  ARCHITECTURE: Hexagonal (Ports & Adapters)                               ║
║  ─────────────────────────────────────────────────────────────────────    ║
║                                                                            ║
║  ┌────────────────────────────────────────────────────────┐              ║
║  │               PRIMARY ADAPTERS (UI)                    │              ║
║  │                   CLI / REST API                        │              ║
║  └─────────────────────┬──────────────────────────────────┘              ║
║                        │                                                  ║
║  ┌─────────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────────────┐              ║
║  │            APPLICATION LAYER (Use Cases)                │              ║
║  │    - Command Handlers (Write)                          │              ║
║  │    - Query Handlers (Read)                             │              ║
║  │    - BankingService (Facade)                           │              ║
║  └─────────────────────┬──────────────────────────────────┘              ║
║                        │                                                  ║
║  ┌─────────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────────────┐              ║
║  │      DOMAIN LAYER (Pure Business Logic)                │              ║
║  │                                                          │              ║
║  │  VALUE OBJECTS:                                         │              ║
║  │    [OK] Money (immutable, operations)                     │              ║
║  │    [OK] AccountNumber, CustomerId, Email                  │              ║
║  │                                                          │              ║
║  │  ENTITIES & AGGREGATES:                                 │              ║
║  │    [OK] Account (Aggregate Root)                          │              ║
║  │      - Event Sourcing                                   │              ║
║  │      - Commands -> Events                                │              ║
║  │      - State reconstruction                             │              ║
║  │                                                          │              ║
║  │  DOMAIN EVENTS:                                         │              ║
║  │    [OK] AccountOpened, MoneyDeposited                     │              ║
║  │    [OK] MoneyWithdrawn, AccountClosed                     │              ║
║  │                                                          │              ║
║  │  DOMAIN SERVICES:                                       │              ║
║  │    [OK] TransferService (2 aggregates)                    │              ║
║  │                                                          │              ║
║  │  PORTS (Interfaces):                                    │              ║
║  │    [OK] EventStore                                         │              ║
║  │    [OK] AccountRepository                                  │              ║
║  │    [OK] EventPublisher                                     │              ║
║  └─────────────────────┬──────────────────────────────────┘              ║
║                        │                                                  ║
║  ┌─────────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────────────┐              ║
║  │    INFRASTRUCTURE (Technical Details)                   │              ║
║  │                                                          │              ║
║  │  ADAPTERS (Implementations):                           │              ║
║  │    [OK] InMemoryEventStore                                │              ║
║  │    [OK] InMemoryAccountRepository                         │              ║
║  │    [OK] InMemoryEventPublisher                            │              ║
║  │    [OK] UnitOfWork (transactions)                         │              ║
║  └──────────────────────────────────────────────────────────┘              ║
║                                                                            ║
╠═══════════════════════════════════════════════════════════════════════════╣
║                                                                            ║
║  PATTERNS & PRINCIPLES APPLIED                                            ║
║  ──────────────────────────────────────────────────────────────────       ║
║                                                                            ║
║  [MESURE] ARCHITECTURE:                                                         ║
║     [OK] Hexagonal Architecture (Ports & Adapters)                         ║
║     [OK] Event Sourcing (complete history)                                 ║
║     [OK] CQRS (Command Query Separation)                                   ║
║     [OK] Domain-Driven Design (DDD)                                        ║
║                                                                            ║
║  [OBJECTIF] DESIGN PATTERNS:                                                      ║
║     [OK] Repository Pattern                                                 ║
║     [OK] Unit of Work                                                       ║
║     [OK] Factory Method                                                     ║
║     [OK] Command Pattern (CQRS)                                            ║
║     [OK] Observer Pattern (EventPublisher)                                 ║
║                                                                            ║
║  [JAPANESE_SYMBOL_FOR_BEGINNER] SOLID PRINCIPLES:                                                     ║
║     [OK] SRP: Each class one responsibility                                ║
║     [OK] OCP: Open for extension (strategies)                              ║
║     [OK] LSP: Proper substitution                                          ║
║     [OK] ISP: Focused interfaces (Protocols)                               ║
║     [OK] DIP: Depend on abstractions (Ports)                               ║
║                                                                            ║
║  [TEST] TESTING:                                                              ║
║     [OK] TDD (Test Driven Development)                                     ║
║     [OK] Unit Tests (Value Objects, Entities)                              ║
║     [OK] Integration Tests (Infrastructure)                                ║
║     [OK] Acceptance Tests (End-to-End)                                     ║
║                                                                            ║
╠═══════════════════════════════════════════════════════════════════════════╣
║                                                                            ║
║  KEY FEATURES                                                             ║
║  ─────────────────────────────────────────────────────────────────────    ║
║                                                                            ║
║  [OK] Complete Event Sourcing implementation                               ║
║  [OK] Time travel (reconstruct any past state)                             ║
║  [OK] Perfect audit trail                                                   ║
║  [OK] Optimistic locking (concurrency)                                     ║
║  [OK] Domain events (notifications)                                        ║
║  [OK] CQRS (separate read/write models)                                    ║
║  [OK] Immutable value objects                                              ║
║  [OK] Aggregate root with invariants                                       ║
║  [OK] CLI interface                                                         ║
║  [OK] 100% testable (pure domain)                                          ║
║                                                                            ║
╠═══════════════════════════════════════════════════════════════════════════╣
║                                                                            ║
║  BENEFITS OF THIS ARCHITECTURE                                            ║
║  ─────────────────────────────────────────────────────────────────────    ║
║                                                                            ║
║  [OBJECTIF] Business Logic Isolated                                              ║
║     Domain has ZERO dependencies on infrastructure                        ║
║                                                                            ║
║  [SYNC] Flexible & Maintainable                                              ║
║     Change DB, UI, or any adapter without touching domain                ║
║                                                                            ║
║  [TEST] Highly Testable                                                       ║
║     Test domain logic without any infrastructure                         ║
║                                                                            ║
║  [DOC] Complete History                                                      ║
║     Every change recorded, nothing lost                                   ║
║                                                                            ║
║  [RECHERCHE] Debugging & Auditing                                                 ║
║     Replay events to understand what happened                            ║
║                                                                            ║
║  [RAPIDE] Scalable                                                              ║
║     CQRS allows separate scaling of reads/writes                         ║
║                                                                            ║
╚═══════════════════════════════════════════════════════════════════════════╝
"""


def print_summary():
    """Afficher récapitulatif."""
    print(BANKING_SUMMARY)


"""
═══════════════════════════════════════════════════════════════════════════
MAIN : DÉMONSTRATIONS
═══════════════════════════════════════════════════════════════════════════
"""

def main():
    """Point d'entrée principal."""
    import sys
    
    if len(sys.argv) > 1:
        mode = sys.argv[1]
        
        if mode == "demo":
            # Démonstration Event Sourcing
            demo_event_sourcing()
            
        elif mode == "comparison":
            # Comparaison approches
            comparison_with_without_event_sourcing()
            
        elif mode == "cli":
            # Interface CLI
            app = create_banking_application()
            cli = BankingCLI(app)
            cli.start()
            
        elif mode == "summary":
            # Récapitulatif
            print_summary()
            
        else:
            print("Unknown mode. Use: demo, comparison, cli, or summary")
    
    else:
        # Par défaut : démonstration puis CLI
        print("Starting banking system...")
        print("\n1. Running Event Sourcing demo...")
        app = demo_event_sourcing()
        
        print("\n\n2. Showing comparison...")
        comparison_with_without_event_sourcing()
        
        print("\n\n3. Starting CLI...")
        cli = BankingCLI(app)
        cli.start()


if __name__ == '__main__':
    # Exécution :
    # python banking_system.py demo        # Démonstration
    # python banking_system.py comparison  # Comparaison
    # python banking_system.py cli         # Interface CLI
    # python banking_system.py summary     # Récapitulatif
    
    main()
    
    # Ou exécuter tests :
    # pytest banking_system.py -v


# ============================================================================
# [DOCS] PROJET PRATIQUE 3 : SYSTÈME DE BIBLIOTHÈQUE
# ============================================================================
#
# [OBJECTIF] OBJECTIF :
# Créer système de bibliothèque avec Clean Architecture et DDD approfondi :
# - Clean Architecture (Uncle Bob)
# - Domain-Driven Design tactique avancé
# - Bounded Contexts
# - Aggregates complexes
# - Value Objects riches
# - Domain Services
# - Specification Pattern
# - Business Rules explicites
#
# [DOCS] CONCEPTS APPLIQUÉS :
# [OK] Clean Architecture (4 couches)
# [OK] DDD Tactique complet
# [OK] Bounded Contexts
# [OK] Ubiquitous Language
# [OK] Aggregates complexes
# [OK] Business Invariants
# [OK] Specification Pattern
# [OK] Policy Pattern
#
# [TEMPS] TEMPS : ~12-15 heures de lecture et pratique
# [IDEE] NIVEAU : Expert - DDD et Clean Architecture
# ============================================================================


"""
═══════════════════════════════════════════════════════════════════════════
[CONSTRUCTION] CLEAN ARCHITECTURE (Uncle Bob)
═══════════════════════════════════════════════════════════════════════════

[IDEE] CLEAN ARCHITECTURE

4 couches concentriques (cercles) :

         ┌─────────────────────────────────┐
         │   4. FRAMEWORKS & DRIVERS       │  <- Détails (DB, UI, Web)
         │      (Infrastructure)           │
         └──────────────┬──────────────────┘
                        │
         ┌──────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────┐
         │   3. INTERFACE ADAPTERS         │  <- Controllers, Presenters
         │      (Application)              │     Gateways
         └──────────────┬──────────────────┘
                        │
         ┌──────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────┐
         │   2. USE CASES                  │  <- Application Business Rules
         │      (Application)              │
         └──────────────┬──────────────────┘
                        │
         ┌──────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────┐
         │   1. ENTITIES                   │  <- Enterprise Business Rules
         │      (Domain)                   │     (Le cœur)
         └─────────────────────────────────┘


RÈGLE DE DÉPENDANCE :
-> Dépendances pointent vers l'intérieur UNIQUEMENT
-> Cercles intérieurs ne connaissent PAS cercles extérieurs
-> Domain est pur (0 dépendance externe)


STRUCTURE PROJET :

library_system/
├── domain/                    # Couche 1 : ENTITIES (0 deps)
│   ├── model/                # Entités, Value Objects, Aggregates
│   │   ├── book.py
│   │   ├── member.py
│   │   ├── loan.py
│   │   ├── reservation.py
│   │   └── value_objects.py
│   ├── services/             # Domain Services
│   │   ├── loan_policy.py
│   │   └── fine_calculator.py
│   ├── specifications/       # Specification Pattern
│   │   └── book_specs.py
│   └── exceptions/           # Domain Exceptions
│       └── domain_errors.py
├── application/              # Couches 2-3 : USE CASES + ADAPTERS
│   ├── use_cases/           # Use Cases (interactors)
│   │   ├── borrow_book.py
│   │   ├── return_book.py
│   │   └── reserve_book.py
│   ├── ports/               # Interfaces (Dependency Inversion)
│   │   ├── repositories.py
│   │   └── services.py
│   └── dto/                 # Data Transfer Objects
│       └── library_dto.py
├── infrastructure/           # Couche 4 : FRAMEWORKS & DRIVERS
│   ├── persistence/
│   │   └── in_memory_repositories.py
│   ├── notification/
│   │   └── email_service.py
│   └── external/
│       └── isbn_service.py
└── tests/
    ├── domain/
    ├── application/
    └── integration/
"""


# ============================================================================
# [PACKAGE] PARTIE 1 : DOMAIN LAYER (ENTITIES - COUCHE 1)
# ============================================================================

from __future__ import annotations
from typing import List, Optional, Set, Protocol
from datetime import datetime, date, timedelta
from decimal import Decimal
from enum import Enum
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
import uuid


"""
═══════════════════════════════════════════════════════════════════════════
UBIQUITOUS LANGUAGE (Langage Métier)
═══════════════════════════════════════════════════════════════════════════

[IDEE] UBIQUITOUS LANGUAGE

Vocabulaire partagé entre experts métier et développeurs.
Utilisé dans code ET conversations.


BOUNDED CONTEXT : BIBLIOTHÈQUE

Termes métier :
- Member (Membre) : Utilisateur de la bibliothèque
- Book (Livre) : Ouvrage disponible
- Copy (Exemplaire) : Instance physique d'un livre
- Loan (Prêt) : Emprunt d'un exemplaire par un membre
- Reservation (Réservation) : Réservation d'un livre
- Fine (Amende) : Pénalité pour retard
- MembershipType : Type d'adhésion (Standard, Premium, Student)
- LoanPolicy : Politique de prêt (durée, nombre max)


BUSINESS RULES :
1. Membre peut emprunter max N livres simultanément (selon type)
2. Durée prêt = 14 jours (Standard), 21 jours (Premium), 7 jours (Student)
3. Amende = 0.50€/jour de retard
4. Membre avec amendes > 10€ ne peut pas emprunter
5. Livre peut être réservé si tous exemplaires empruntés
6. Réservation expire après 3 jours
7. Exemplaire endommagé -> amende forfaitaire 20€
"""


# ============================================================================
# VALUE OBJECTS (DDD Tactique)
# ============================================================================

@dataclass(frozen=True)
class ISBN:
    """
    [IDEE] VALUE OBJECT : International Standard Book Number
    
    Format : ISBN-13 (13 chiffres)
    Exemple : 978-3-16-148410-0
    
    Immuable, valide à la construction.
    """
    
    value: str
    
    def __post_init__(self):
        """Validation ISBN-13."""
        # Retirer tirets pour validation
        digits = self.value.replace("-", "").replace(" ", "")
        
        if len(digits) != 13:
            raise ValueError(f"ISBN must be 13 digits, got {len(digits)}")
        
        if not digits.isdigit():
            raise ValueError("ISBN must contain only digits")
        
        # Vérifier checksum (algorithme ISBN-13)
        if not self._is_valid_checksum(digits):
            raise ValueError("Invalid ISBN checksum")
    
    def _is_valid_checksum(self, digits: str) -> bool:
        """
        Vérifier checksum ISBN-13.
        
        Algorithme : somme alternée * (1 ou 3) mod 10 = 0
        """
        total = sum(
            int(digit) * (1 if i % 2 == 0 else 3)
            for i, digit in enumerate(digits)
        )
        return total % 10 == 0
    
    def __str__(self) -> str:
        return self.value
    
    @classmethod
    def from_digits(cls, digits: str) -> ISBN:
        """
        Créer ISBN depuis chiffres (sans tirets).
        
        Formate automatiquement.
        """
        if len(digits) != 13:
            raise ValueError("Must be 13 digits")
        
        # Format : 978-3-16-148410-0
        formatted = f"{digits[:3]}-{digits[3]}-{digits[4:6]}-{digits[6:12]}-{digits[12]}"
        return cls(formatted)


@dataclass(frozen=True)
class MemberId:
    """
    [IDEE] VALUE OBJECT : Identifiant membre
    
    Format : MEM-XXXXXX
    """
    
    value: str
    
    def __post_init__(self):
        """Validation."""
        if not self.value.startswith("MEM-"):
            raise ValueError("MemberId must start with 'MEM-'")
        
        if len(self.value) != 10:  # MEM-XXXXXX
            raise ValueError("MemberId must be 10 characters")
    
    def __str__(self) -> str:
        return self.value
    
    @classmethod
    def generate(cls) -> MemberId:
        """Générer nouvel ID."""
        import random
        code = ''.join(str(random.randint(0, 9)) for _ in range(6))
        return cls(f"MEM-{code}")


@dataclass(frozen=True)
class Money:
    """
    [IDEE] VALUE OBJECT : Montant d'argent
    
    Réutilisé du projet bancaire.
    """
    
    amount: Decimal
    currency: str = "EUR"
    
    def __post_init__(self):
        """Validation."""
        if not isinstance(self.amount, Decimal):
            raise TypeError("Amount must be Decimal")
        
        if self.amount < 0:
            raise ValueError("Amount cannot be negative")
    
    def __add__(self, other: Money) -> Money:
        """Addition."""
        if self.currency != other.currency:
            raise ValueError("Cannot add different currencies")
        return Money(self.amount + other.amount, self.currency)
    
    def __sub__(self, other: Money) -> Money:
        """Soustraction."""
        if self.currency != other.currency:
            raise ValueError("Cannot subtract different currencies")
        result = self.amount - other.amount
        if result < 0:
            raise ValueError("Result cannot be negative")
        return Money(result, self.currency)
    
    def __mul__(self, multiplier: int) -> Money:
        """Multiplication."""
        return Money(self.amount * multiplier, self.currency)
    
    def __lt__(self, other: Money) -> bool:
        """Comparaison <"""
        if self.currency != other.currency:
            raise ValueError("Cannot compare different currencies")
        return self.amount < other.amount
    
    def __le__(self, other: Money) -> bool:
        """Comparaison <="""
        if self.currency != other.currency:
            raise ValueError("Cannot compare different currencies")
        return self.amount <= other.amount
    
    def __gt__(self, other: Money) -> bool:
        """Comparaison >"""
        if self.currency != other.currency:
            raise ValueError("Cannot compare different currencies")
        return self.amount > other.amount
    
    def __ge__(self, other: Money) -> bool:
        """Comparaison >="""
        if self.currency != other.currency:
            raise ValueError("Cannot compare different currencies")
        return self.amount >= other.amount
    
    def is_zero(self) -> bool:
        """Vérifier si montant zéro."""
        return self.amount == 0
    
    def __str__(self) -> str:
        return f"{self.amount:.2f} {self.currency}"
    
    @classmethod
    def zero(cls, currency: str = "EUR") -> Money:
        """Créer montant zéro."""
        return cls(Decimal("0"), currency)


@dataclass(frozen=True)
class EmailAddress:
    """
    [IDEE] VALUE OBJECT : Adresse email
    """
    
    value: str
    
    def __post_init__(self):
        """Validation email."""
        if not self.value or '@' not in self.value:
            raise ValueError(f"Invalid email: {self.value}")
        
        if '.' not in self.value.split('@')[1]:
            raise ValueError(f"Invalid email domain: {self.value}")
        
        # Normalisation
        object.__setattr__(self, 'value', self.value.lower().strip())
    
    def __str__(self) -> str:
        return self.value


@dataclass(frozen=True)
class DateRange:
    """
    [IDEE] VALUE OBJECT : Période de dates
    
    Représente intervalle entre deux dates.
    """
    
    start_date: date
    end_date: date
    
    def __post_init__(self):
        """Validation."""
        if self.end_date < self.start_date:
            raise ValueError("End date must be after start date")
    
    def duration_days(self) -> int:
        """Durée en jours."""
        return (self.end_date - self.start_date).days
    
    def contains(self, check_date: date) -> bool:
        """Vérifier si date dans période."""
        return self.start_date <= check_date <= self.end_date
    
    def overlaps(self, other: DateRange) -> bool:
        """Vérifier si périodes se chevauchent."""
        return (
            self.start_date <= other.end_date and
            other.start_date <= self.end_date
        )
    
    def __str__(self) -> str:
        return f"{self.start_date} to {self.end_date}"


# ============================================================================
# ENUMERATIONS (Types métier)
# ============================================================================

class MembershipType(Enum):
    """
    Type d'adhésion membre.
    
    Détermine politique de prêt.
    """
    STANDARD = "standard"      # Adhésion basique
    PREMIUM = "premium"        # Adhésion premium (plus de privilèges)
    STUDENT = "student"        # Étudiant (durées courtes)


class MemberStatus(Enum):
    """Statut du membre."""
    ACTIVE = "active"          # Actif, peut emprunter
    SUSPENDED = "suspended"    # Suspendu (amendes impayées)
    EXPIRED = "expired"        # Adhésion expirée


class BookStatus(Enum):
    """Statut d'un exemplaire de livre."""
    AVAILABLE = "available"    # Disponible pour emprunt
    BORROWED = "borrowed"      # Actuellement emprunté
    RESERVED = "reserved"      # Réservé pour quelqu'un
    DAMAGED = "damaged"        # Endommagé
    LOST = "lost"             # Perdu


class LoanStatus(Enum):
    """Statut d'un prêt."""
    ACTIVE = "active"          # Prêt en cours
    RETURNED = "returned"      # Livre retourné
    OVERDUE = "overdue"        # En retard


class ReservationStatus(Enum):
    """Statut d'une réservation."""
    PENDING = "pending"        # En attente
    READY = "ready"           # Livre disponible
    FULFILLED = "fulfilled"    # Réservation satisfaite (livre emprunté)
    EXPIRED = "expired"       # Réservation expirée
    CANCELLED = "cancelled"    # Annulée


# ============================================================================
# ENTITIES (DDD)
# ============================================================================

class Book:
    """
    [IDEE] ENTITY : Livre (métadonnées)
    
    Représente un TITRE de livre.
    Un livre peut avoir plusieurs exemplaires (copies).
    
    Attributes:
        isbn: ISBN unique
        title: Titre
        author: Auteur
        publisher: Éditeur
        publication_year: Année publication
        category: Catégorie
    """
    
    def __init__(
        self,
        isbn: ISBN,
        title: str,
        author: str,
        publisher: str = "",
        publication_year: int = None,
        category: str = "General"
    ):
        """
        Initialise livre.
        
        Args:
            isbn: ISBN unique
            title: Titre du livre
            author: Auteur
            publisher: Éditeur
            publication_year: Année de publication
            category: Catégorie
        """
        # Validation
        if not title or not title.strip():
            raise ValueError("Title cannot be empty")
        
        if not author or not author.strip():
            raise ValueError("Author cannot be empty")
        
        # Identité (ISBN)
        self._isbn = isbn
        
        # Attributs
        self._title = title.strip()
        self._author = author.strip()
        self._publisher = publisher.strip()
        self._publication_year = publication_year
        self._category = category
        
        # Collection d'exemplaires (copies)
        self._copies: List[BookCopy] = []
    
    # Properties (lecture seule)
    @property
    def isbn(self) -> ISBN:
        """ISBN (identité)."""
        return self._isbn
    
    @property
    def title(self) -> str:
        """Titre."""
        return self._title
    
    @property
    def author(self) -> str:
        """Auteur."""
        return self._author
    
    @property
    def publisher(self) -> str:
        """Éditeur."""
        return self._publisher
    
    @property
    def publication_year(self) -> Optional[int]:
        """Année de publication."""
        return self._publication_year
    
    @property
    def category(self) -> str:
        """Catégorie."""
        return self._category
    
    @property
    def copies(self) -> List[BookCopy]:
        """Liste des exemplaires."""
        return self._copies.copy()
    
    # Méthodes métier
    def add_copy(self, copy: BookCopy) -> None:
        """
        Ajouter exemplaire.
        
        Args:
            copy: Exemplaire à ajouter
        """
        if copy in self._copies:
            raise ValueError("Copy already added")
        
        self._copies.append(copy)
    
    def get_available_copies(self) -> List[BookCopy]:
        """
        Obtenir exemplaires disponibles.
        
        Returns:
            Liste des exemplaires disponibles
        """
        return [
            copy for copy in self._copies
            if copy.status == BookStatus.AVAILABLE
        ]
    
    def has_available_copy(self) -> bool:
        """Vérifier si au moins un exemplaire disponible."""
        return len(self.get_available_copies()) > 0
    
    def total_copies(self) -> int:
        """Nombre total d'exemplaires."""
        return len(self._copies)
    
    def available_count(self) -> int:
        """Nombre d'exemplaires disponibles."""
        return len(self.get_available_copies())
    
    def __eq__(self, other) -> bool:
        """Égalité basée sur ISBN."""
        if not isinstance(other, Book):
            return False
        return self.isbn == other.isbn
    
    def __hash__(self) -> int:
        """Hash basé sur ISBN."""
        return hash(self.isbn)
    
    def __repr__(self) -> str:
        return f"Book(isbn={self.isbn}, title={self.title!r}, author={self.author!r})"
    
    def __str__(self) -> str:
        return f"{self.title} by {self.author} (ISBN: {self.isbn})"


class BookCopy:
    """
    [IDEE] ENTITY : Exemplaire de livre
    
    Instance PHYSIQUE d'un livre.
    Plusieurs exemplaires peuvent exister pour un même Book.
    
    Attributes:
        copy_id: ID unique de l'exemplaire
        book: Livre auquel appartient cet exemplaire
        status: Statut actuel
        acquisition_date: Date d'acquisition
        condition: État physique
    """
    
    def __init__(
        self,
        copy_id: str,
        book: Book,
        acquisition_date: date = None
    ):
        """
        Initialise exemplaire.
        
        Args:
            copy_id: ID unique
            book: Livre parent
            acquisition_date: Date d'acquisition
        """
        # Identité
        self._copy_id = copy_id
        
        # Référence au livre
        self._book = book
        
        # État
        self._status = BookStatus.AVAILABLE
        self._acquisition_date = acquisition_date or date.today()
        self._condition = "Good"  # Good, Fair, Poor
    
    @property
    def copy_id(self) -> str:
        """ID exemplaire."""
        return self._copy_id
    
    @property
    def book(self) -> Book:
        """Livre parent."""
        return self._book
    
    @property
    def status(self) -> BookStatus:
        """Statut actuel."""
        return self._status
    
    @property
    def acquisition_date(self) -> date:
        """Date d'acquisition."""
        return self._acquisition_date
    
    @property
    def condition(self) -> str:
        """État physique."""
        return self._condition
    
    # Méthodes métier
    def borrow(self) -> None:
        """
        Emprunter exemplaire.
        
        Raises:
            ValueError: Si exemplaire pas disponible
        """
        if self._status != BookStatus.AVAILABLE:
            raise ValueError(f"Copy {self._copy_id} is not available")
        
        self._status = BookStatus.BORROWED
    
    def return_copy(self) -> None:
        """Retourner exemplaire."""
        if self._status != BookStatus.BORROWED:
            raise ValueError(f"Copy {self._copy_id} is not borrowed")
        
        self._status = BookStatus.AVAILABLE
    
    def mark_damaged(self, new_condition: str = "Poor") -> None:
        """
        Marquer comme endommagé.
        
        Args:
            new_condition: Nouvel état
        """
        self._status = BookStatus.DAMAGED
        self._condition = new_condition
    
    def mark_lost(self) -> None:
        """Marquer comme perdu."""
        self._status = BookStatus.LOST
    
    def is_available(self) -> bool:
        """Vérifier si disponible."""
        return self._status == BookStatus.AVAILABLE
    
    def __eq__(self, other) -> bool:
        """Égalité basée sur copy_id."""
        if not isinstance(other, BookCopy):
            return False
        return self._copy_id == other._copy_id
    
    def __hash__(self) -> int:
        """Hash basé sur copy_id."""
        return hash(self._copy_id)
    
    def __repr__(self) -> str:
        return f"BookCopy(id={self._copy_id}, book={self._book.title}, status={self._status.value})"


# Ce fichier continue...
# ============================================================================

# ============================================================================
# [DOCS] PROJET PRATIQUE 3 : SYSTÈME DE BIBLIOTHÈQUE (PARTIE 2)
# ============================================================================
#
# Suite du projet : Aggregates complexes avec DDD tactique approfondi
#
# [OBJECTIF] CONTENU :
# - Aggregate Root : Member (avec invariants complexes)
# - Aggregate : Loan (prêt avec business rules)
# - Aggregate : Reservation (réservation)
# - Entity : Fine (amende avec calculs)
# - Business Invariants stricts
# - Encapsulation parfaite
#
# ============================================================================


# Suite de la Partie 1...

from typing import List, Optional, Set
from datetime import datetime, date, timedelta
from decimal import Decimal
from enum import Enum


"""
═══════════════════════════════════════════════════════════════════════════
AGGREGATE ROOT : MEMBER (Membre)
═══════════════════════════════════════════════════════════════════════════

[IDEE] AGGREGATE ROOT

Garantit cohérence et invariants métier.
Contrôle accès aux entités enfants.


INVARIANTS MÉTIER (Member) :
1. Membre avec amendes > 10€ ne peut PAS emprunter
2. Membre suspendu ne peut PAS emprunter
3. Adhésion expirée -> statut EXPIRED automatiquement
4. Nombre max emprunts selon MembershipType :
   - STANDARD : 3 livres max
   - PREMIUM : 10 livres max
   - STUDENT : 5 livres max
5. Email unique et valide obligatoire
"""

class Member:
    """
    [IDEE] AGGREGATE ROOT : Membre de la bibliothèque
    
    Coordonne :
    - Loans (prêts en cours)
    - Fines (amendes)
    - Reservations (réservations)
    
    Garantit invariants métier complexes.
    
    Attributes:
        member_id: Identifiant unique
        name: Nom complet
        email: Adresse email
        membership_type: Type d'adhésion
        status: Statut actuel
        joined_date: Date d'inscription
        membership_expiry: Date d'expiration adhésion
    """
    
    # Configuration politique de prêt par type
    MAX_LOANS_BY_TYPE = {
        MembershipType.STANDARD: 3,
        MembershipType.PREMIUM: 10,
        MembershipType.STUDENT: 5
    }
    
    LOAN_DURATION_DAYS_BY_TYPE = {
        MembershipType.STANDARD: 14,
        MembershipType.PREMIUM: 21,
        MembershipType.STUDENT: 7
    }
    
    # Seuil d'amendes
    MAX_FINE_AMOUNT = Money(Decimal("10.00"), "EUR")
    
    def __init__(
        self,
        member_id: MemberId,
        name: str,
        email: EmailAddress,
        membership_type: MembershipType,
        joined_date: date = None
    ):
        """
        Initialise membre.
        
        Args:
            member_id: ID unique
            name: Nom complet
            email: Email
            membership_type: Type d'adhésion
            joined_date: Date d'inscription
            
        Raises:
            ValueError: Si données invalides
        """
        # Validation
        if not name or not name.strip():
            raise ValueError("Name cannot be empty")
        
        # Identité
        self._member_id = member_id
        
        # Attributs
        self._name = name.strip()
        self._email = email
        self._membership_type = membership_type
        self._status = MemberStatus.ACTIVE
        self._joined_date = joined_date or date.today()
        
        # Adhésion valide 1 an par défaut
        self._membership_expiry = self._joined_date + timedelta(days=365)
        
        # Collections (entités enfants dans l'aggregate)
        self._active_loans: List[Loan] = []
        self._loan_history: List[Loan] = []
        self._fines: List[Fine] = []
        self._reservations: List[Reservation] = []
    
    # ========================================================================
    # PROPERTIES (lecture seule)
    # ========================================================================
    
    @property
    def member_id(self) -> MemberId:
        """ID membre."""
        return self._member_id
    
    @property
    def name(self) -> str:
        """Nom."""
        return self._name
    
    @property
    def email(self) -> EmailAddress:
        """Email."""
        return self._email
    
    @property
    def membership_type(self) -> MembershipType:
        """Type d'adhésion."""
        return self._membership_type
    
    @property
    def status(self) -> MemberStatus:
        """Statut actuel."""
        return self._status
    
    @property
    def joined_date(self) -> date:
        """Date d'inscription."""
        return self._joined_date
    
    @property
    def membership_expiry(self) -> date:
        """Date d'expiration adhésion."""
        return self._membership_expiry
    
    @property
    def active_loans(self) -> List[Loan]:
        """Prêts en cours (copie défensive)."""
        return self._active_loans.copy()
    
    @property
    def fines(self) -> List[Fine]:
        """Amendes (copie défensive)."""
        return self._fines.copy()
    
    @property
    def reservations(self) -> List[Reservation]:
        """Réservations (copie défensive)."""
        return self._reservations.copy()
    
    # ========================================================================
    # BUSINESS METHODS (Commands)
    # ========================================================================
    
    def borrow_book(self, book_copy: BookCopy, loan_date: date = None) -> Loan:
        """
        [IDEE] COMMAND : Emprunter un livre
        
        Applique TOUS les invariants métier.
        
        Args:
            book_copy: Exemplaire à emprunter
            loan_date: Date d'emprunt (aujourd'hui par défaut)
            
        Returns:
            Loan créé
            
        Raises:
            ValueError: Si emprunt impossible (invariants violés)
        """
        # INVARIANT 1 : Vérifier statut membre
        self._ensure_can_borrow()
        
        # INVARIANT 2 : Vérifier limite emprunts
        if not self._can_borrow_more_books():
            max_allowed = self.MAX_LOANS_BY_TYPE[self._membership_type]
            raise ValueError(
                f"Maximum loans reached ({max_allowed} for {self._membership_type.value})"
            )
        
        # INVARIANT 3 : Vérifier exemplaire disponible
        if not book_copy.is_available():
            raise ValueError(f"Book copy {book_copy.copy_id} is not available")
        
        # Calculer date de retour selon type adhésion
        loan_date = loan_date or date.today()
        duration_days = self.LOAN_DURATION_DAYS_BY_TYPE[self._membership_type]
        due_date = loan_date + timedelta(days=duration_days)
        
        # Créer prêt
        loan = Loan(
            loan_id=self._generate_loan_id(),
            member=self,
            book_copy=book_copy,
            loan_date=loan_date,
            due_date=due_date
        )
        
        # Changer état exemplaire
        book_copy.borrow()
        
        # Ajouter au prêts actifs
        self._active_loans.append(loan)
        
        return loan
    
    def return_book(self, loan: Loan, return_date: date = None) -> Optional[Fine]:
        """
        [IDEE] COMMAND : Retourner un livre
        
        Calcule amende si retard.
        
        Args:
            loan: Prêt à retourner
            return_date: Date de retour
            
        Returns:
            Fine si retard, None sinon
            
        Raises:
            ValueError: Si prêt pas trouvé ou déjà retourné
        """
        # Vérifier que prêt appartient à ce membre
        if loan not in self._active_loans:
            raise ValueError(f"Loan {loan.loan_id} not found in active loans")
        
        return_date = return_date or date.today()
        
        # Retourner livre (calcule amende si retard)
        fine = loan.return_book(return_date)
        
        # Si amende créée, l'ajouter
        if fine:
            self._fines.append(fine)
            
            # Vérifier si dépasse seuil -> suspension
            if self.total_unpaid_fines() > self.MAX_FINE_AMOUNT:
                self._status = MemberStatus.SUSPENDED
        
        # Déplacer vers historique
        self._active_loans.remove(loan)
        self._loan_history.append(loan)
        
        return fine
    
    def pay_fine(self, fine: Fine, amount: Money) -> None:
        """
        [IDEE] COMMAND : Payer amende
        
        Args:
            fine: Amende à payer
            amount: Montant payé
            
        Raises:
            ValueError: Si amende pas trouvée ou montant invalide
        """
        if fine not in self._fines:
            raise ValueError(f"Fine not found for this member")
        
        # Payer
        fine.pay(amount)
        
        # Si toutes amendes payées et était suspendu -> réactiver
        if self.total_unpaid_fines().is_zero():
            if self._status == MemberStatus.SUSPENDED:
                self._status = MemberStatus.ACTIVE
    
    def reserve_book(self, book: Book) -> Reservation:
        """
        [IDEE] COMMAND : Réserver un livre
        
        Args:
            book: Livre à réserver
            
        Returns:
            Reservation créée
            
        Raises:
            ValueError: Si réservation impossible
        """
        # Vérifier que membre peut réserver
        self._ensure_can_borrow()
        
        # Vérifier que livre pas déjà disponible
        if book.has_available_copy():
            raise ValueError(
                "Book has available copies, no need to reserve"
            )
        
        # Vérifier pas déjà réservé par ce membre
        for reservation in self._reservations:
            if reservation.book == book and reservation.is_active():
                raise ValueError("Already reserved this book")
        
        # Créer réservation
        reservation = Reservation(
            reservation_id=self._generate_reservation_id(),
            member=self,
            book=book,
            reserved_date=date.today()
        )
        
        self._reservations.append(reservation)
        
        return reservation
    
    def cancel_reservation(self, reservation: Reservation) -> None:
        """
        Annuler réservation.
        
        Args:
            reservation: Réservation à annuler
        """
        if reservation not in self._reservations:
            raise ValueError("Reservation not found")
        
        reservation.cancel()
    
    def renew_membership(self, renewal_date: date = None) -> None:
        """
        Renouveler adhésion.
        
        Args:
            renewal_date: Date de renouvellement
        """
        renewal_date = renewal_date or date.today()
        
        # Nouvelle expiration : +1 an
        self._membership_expiry = renewal_date + timedelta(days=365)
        
        # Réactiver si expiré
        if self._status == MemberStatus.EXPIRED:
            self._status = MemberStatus.ACTIVE
    
    def upgrade_membership(self, new_type: MembershipType) -> None:
        """
        Changer type d'adhésion.
        
        Args:
            new_type: Nouveau type
        """
        self._membership_type = new_type
    
    # ========================================================================
    # QUERIES (Lectures)
    # ========================================================================
    
    def can_borrow(self) -> bool:
        """
        Vérifier si membre PEUT emprunter.
        
        Returns:
            True si peut emprunter
        """
        try:
            self._ensure_can_borrow()
            return self._can_borrow_more_books()
        except ValueError:
            return False
    
    def total_unpaid_fines(self) -> Money:
        """
        Calculer total amendes impayées.
        
        Returns:
            Montant total
        """
        total = Money.zero()
        
        for fine in self._fines:
            if not fine.is_paid():
                total = total + fine.remaining_amount()
        
        return total
    
    def active_loan_count(self) -> int:
        """Nombre de prêts actifs."""
        return len(self._active_loans)
    
    def is_membership_expired(self) -> bool:
        """Vérifier si adhésion expirée."""
        return date.today() > self._membership_expiry
    
    def get_overdue_loans(self) -> List[Loan]:
        """
        Obtenir prêts en retard.
        
        Returns:
            Liste des prêts en retard
        """
        return [
            loan for loan in self._active_loans
            if loan.is_overdue()
        ]
    
    def has_overdue_loans(self) -> bool:
        """Vérifier si a des prêts en retard."""
        return len(self.get_overdue_loans()) > 0
    
    # ========================================================================
    # INVARIANTS (validation business rules)
    # ========================================================================
    
    def _ensure_can_borrow(self) -> None:
        """
        [IDEE] INVARIANT : Vérifier que membre PEUT emprunter
        
        Raises:
            ValueError: Si ne peut pas emprunter
        """
        # Vérifier adhésion expirée
        if self.is_membership_expired():
            self._status = MemberStatus.EXPIRED
            raise ValueError("Membership expired")
        
        # Vérifier statut
        if self._status != MemberStatus.ACTIVE:
            raise ValueError(f"Member status is {self._status.value}, cannot borrow")
        
        # Vérifier amendes
        if self.total_unpaid_fines() > self.MAX_FINE_AMOUNT:
            raise ValueError(
                f"Unpaid fines exceed {self.MAX_FINE_AMOUNT}, cannot borrow"
            )
    
    def _can_borrow_more_books(self) -> bool:
        """
        Vérifier si peut emprunter plus de livres.
        
        Returns:
            True si sous la limite
        """
        max_loans = self.MAX_LOANS_BY_TYPE[self._membership_type]
        return self.active_loan_count() < max_loans
    
    # ========================================================================
    # HELPERS
    # ========================================================================
    
    def _generate_loan_id(self) -> str:
        """Générer ID de prêt."""
        import uuid
        return f"LOAN-{uuid.uuid4().hex[:8].upper()}"
    
    def _generate_reservation_id(self) -> str:
        """Générer ID de réservation."""
        import uuid
        return f"RES-{uuid.uuid4().hex[:8].upper()}"
    
    # ========================================================================
    # REPRÉSENTATION
    # ========================================================================
    
    def __eq__(self, other) -> bool:
        """Égalité basée sur member_id."""
        if not isinstance(other, Member):
            return False
        return self._member_id == other._member_id
    
    def __hash__(self) -> int:
        """Hash basé sur member_id."""
        return hash(self._member_id)
    
    def __repr__(self) -> str:
        return (
            f"Member(id={self._member_id}, name={self._name!r}, "
            f"type={self._membership_type.value}, status={self._status.value})"
        )
    
    def __str__(self) -> str:
        return (
            f"Member: {self._name}\n"
            f"  ID: {self._member_id}\n"
            f"  Type: {self._membership_type.value}\n"
            f"  Status: {self._status.value}\n"
            f"  Active loans: {self.active_loan_count()}\n"
            f"  Unpaid fines: {self.total_unpaid_fines()}"
        )


"""
═══════════════════════════════════════════════════════════════════════════
AGGREGATE : LOAN (Prêt)
═══════════════════════════════════════════════════════════════════════════

[IDEE] AGGREGATE

Représente un prêt de livre.
Calcule automatiquement amendes en cas de retard.


BUSINESS RULES :
- Amende = 0.50€ par jour de retard
- Statut OVERDUE si date actuelle > due_date
- Retour impossible si déjà retourné
"""

class Loan:
    """
    [IDEE] AGGREGATE : Prêt de livre
    
    Gère cycle de vie d'un emprunt.
    Calcule amendes automatiquement.
    
    Attributes:
        loan_id: ID unique du prêt
        member: Membre emprunteur
        book_copy: Exemplaire emprunté
        loan_date: Date d'emprunt
        due_date: Date de retour prévue
        return_date: Date de retour effective
        status: Statut du prêt
    """
    
    # Configuration
    FINE_PER_DAY = Money(Decimal("0.50"), "EUR")
    
    def __init__(
        self,
        loan_id: str,
        member: Member,
        book_copy: BookCopy,
        loan_date: date,
        due_date: date
    ):
        """
        Initialise prêt.
        
        Args:
            loan_id: ID unique
            member: Membre emprunteur
            book_copy: Exemplaire emprunté
            loan_date: Date d'emprunt
            due_date: Date limite de retour
        """
        # Validation
        if due_date < loan_date:
            raise ValueError("Due date must be after loan date")
        
        # Identité
        self._loan_id = loan_id
        
        # Références (dans aggregate Member)
        self._member = member
        self._book_copy = book_copy
        
        # Dates
        self._loan_date = loan_date
        self._due_date = due_date
        self._return_date: Optional[date] = None
        
        # État
        self._status = LoanStatus.ACTIVE
    
    # ========================================================================
    # PROPERTIES
    # ========================================================================
    
    @property
    def loan_id(self) -> str:
        """ID du prêt."""
        return self._loan_id
    
    @property
    def member(self) -> Member:
        """Membre emprunteur."""
        return self._member
    
    @property
    def book_copy(self) -> BookCopy:
        """Exemplaire emprunté."""
        return self._book_copy
    
    @property
    def loan_date(self) -> date:
        """Date d'emprunt."""
        return self._loan_date
    
    @property
    def due_date(self) -> date:
        """Date limite de retour."""
        return self._due_date
    
    @property
    def return_date(self) -> Optional[date]:
        """Date de retour effective."""
        return self._return_date
    
    @property
    def status(self) -> LoanStatus:
        """Statut actuel."""
        return self._status
    
    # ========================================================================
    # BUSINESS METHODS
    # ========================================================================
    
    def return_book(self, return_date: date = None) -> Optional[Fine]:
        """
        [IDEE] COMMAND : Retourner livre
        
        Calcule amende si retard.
        
        Args:
            return_date: Date de retour
            
        Returns:
            Fine si retard, None sinon
            
        Raises:
            ValueError: Si déjà retourné
        """
        if self._status == LoanStatus.RETURNED:
            raise ValueError(f"Loan {self._loan_id} already returned")
        
        return_date = return_date or date.today()
        
        # Enregistrer retour
        self._return_date = return_date
        self._status = LoanStatus.RETURNED
        
        # Retourner exemplaire
        self._book_copy.return_copy()
        
        # Calculer amende si retard
        if return_date > self._due_date:
            days_late = (return_date - self._due_date).days
            fine_amount = self.FINE_PER_DAY * days_late
            
            fine = Fine(
                fine_id=self._generate_fine_id(),
                member=self._member,
                loan=self,
                amount=fine_amount,
                reason=f"Late return: {days_late} days overdue"
            )
            
            return fine
        
        return None
    
    # ========================================================================
    # QUERIES
    # ========================================================================
    
    def is_overdue(self, check_date: date = None) -> bool:
        """
        [IDEE] QUERY : Vérifier si prêt en retard
        
        Args:
            check_date: Date de vérification (aujourd'hui par défaut)
            
        Returns:
            True si en retard
        """
        if self._status == LoanStatus.RETURNED:
            return False
        
        check_date = check_date or date.today()
        
        if check_date > self._due_date:
            self._status = LoanStatus.OVERDUE
            return True
        
        return False
    
    def days_until_due(self, check_date: date = None) -> int:
        """
        Jours restants avant date limite.
        
        Returns:
            Nombre de jours (négatif si en retard)
        """
        check_date = check_date or date.today()
        return (self._due_date - check_date).days
    
    def days_overdue(self, check_date: date = None) -> int:
        """
        Jours de retard.
        
        Returns:
            Nombre de jours de retard (0 si pas en retard)
        """
        if not self.is_overdue(check_date):
            return 0
        
        check_date = check_date or date.today()
        return (check_date - self._due_date).days
    
    def calculate_current_fine(self, check_date: date = None) -> Money:
        """
        Calculer amende actuelle (si retard).
        
        Returns:
            Montant de l'amende
        """
        days = self.days_overdue(check_date)
        if days == 0:
            return Money.zero()
        
        return self.FINE_PER_DAY * days
    
    # ========================================================================
    # HELPERS
    # ========================================================================
    
    def _generate_fine_id(self) -> str:
        """Générer ID d'amende."""
        import uuid
        return f"FINE-{uuid.uuid4().hex[:8].upper()}"
    
    # ========================================================================
    # REPRÉSENTATION
    # ========================================================================
    
    def __eq__(self, other) -> bool:
        """Égalité basée sur loan_id."""
        if not isinstance(other, Loan):
            return False
        return self._loan_id == other._loan_id
    
    def __hash__(self) -> int:
        """Hash basé sur loan_id."""
        return hash(self._loan_id)
    
    def __repr__(self) -> str:
        return (
            f"Loan(id={self._loan_id}, "
            f"book={self._book_copy.book.title}, "
            f"status={self._status.value})"
        )
    
    def __str__(self) -> str:
        return (
            f"Loan {self._loan_id}\n"
            f"  Book: {self._book_copy.book.title}\n"
            f"  Member: {self._member.name}\n"
            f"  Loan date: {self._loan_date}\n"
            f"  Due date: {self._due_date}\n"
            f"  Status: {self._status.value}"
        )


"""
═══════════════════════════════════════════════════════════════════════════
ENTITY : FINE (Amende)
═══════════════════════════════════════════════════════════════════════════
"""

class Fine:
    """
    [IDEE] ENTITY : Amende
    
    Représente pénalité financière.
    Gère paiements partiels.
    
    Attributes:
        fine_id: ID unique
        member: Membre concerné
        loan: Prêt à l'origine (optionnel)
        amount: Montant total
        paid_amount: Montant déjà payé
        reason: Raison de l'amende
        issued_date: Date d'émission
    """
    
    def __init__(
        self,
        fine_id: str,
        member: Member,
        amount: Money,
        reason: str,
        loan: Optional[Loan] = None,
        issued_date: date = None
    ):
        """
        Initialise amende.
        
        Args:
            fine_id: ID unique
            member: Membre concerné
            amount: Montant total
            reason: Raison
            loan: Prêt à l'origine (optionnel)
            issued_date: Date d'émission
        """
        # Identité
        self._fine_id = fine_id
        
        # Références
        self._member = member
        self._loan = loan
        
        # Montants
        self._amount = amount
        self._paid_amount = Money.zero()
        
        # Métadonnées
        self._reason = reason
        self._issued_date = issued_date or date.today()
    
    # ========================================================================
    # PROPERTIES
    # ========================================================================
    
    @property
    def fine_id(self) -> str:
        """ID amende."""
        return self._fine_id
    
    @property
    def member(self) -> Member:
        """Membre concerné."""
        return self._member
    
    @property
    def loan(self) -> Optional[Loan]:
        """Prêt à l'origine."""
        return self._loan
    
    @property
    def amount(self) -> Money:
        """Montant total."""
        return self._amount
    
    @property
    def paid_amount(self) -> Money:
        """Montant payé."""
        return self._paid_amount
    
    @property
    def reason(self) -> str:
        """Raison."""
        return self._reason
    
    @property
    def issued_date(self) -> date:
        """Date d'émission."""
        return self._issued_date
    
    # ========================================================================
    # BUSINESS METHODS
    # ========================================================================
    
    def pay(self, amount: Money) -> None:
        """
        [IDEE] COMMAND : Payer amende (total ou partiel)
        
        Args:
            amount: Montant à payer
            
        Raises:
            ValueError: Si montant invalide
        """
        if amount <= Money.zero():
            raise ValueError("Payment amount must be positive")
        
        remaining = self.remaining_amount()
        
        if amount > remaining:
            raise ValueError(
                f"Payment amount {amount} exceeds remaining {remaining}"
            )
        
        self._paid_amount = self._paid_amount + amount
    
    # ========================================================================
    # QUERIES
    # ========================================================================
    
    def remaining_amount(self) -> Money:
        """
        Montant restant à payer.
        
        Returns:
            Montant restant
        """
        return self._amount - self._paid_amount
    
    def is_paid(self) -> bool:
        """
        Vérifier si amende totalement payée.
        
        Returns:
            True si payée
        """
        return self.remaining_amount().is_zero()
    
    def is_partially_paid(self) -> bool:
        """Vérifier si paiement partiel."""
        return self._paid_amount > Money.zero() and not self.is_paid()
    
    # ========================================================================
    # REPRÉSENTATION
    # ========================================================================
    
    def __eq__(self, other) -> bool:
        """Égalité basée sur fine_id."""
        if not isinstance(other, Fine):
            return False
        return self._fine_id == other._fine_id
    
    def __hash__(self) -> int:
        """Hash basé sur fine_id."""
        return hash(self._fine_id)
    
    def __repr__(self) -> str:
        return f"Fine(id={self._fine_id}, amount={self._amount}, paid={self._paid_amount})"
    
    def __str__(self) -> str:
        status = "PAID" if self.is_paid() else "UNPAID"
        return (
            f"Fine {self._fine_id} [{status}]\n"
            f"  Amount: {self._amount}\n"
            f"  Paid: {self._paid_amount}\n"
            f"  Remaining: {self.remaining_amount()}\n"
            f"  Reason: {self._reason}"
        )


"""
═══════════════════════════════════════════════════════════════════════════
AGGREGATE : RESERVATION (Réservation)
═══════════════════════════════════════════════════════════════════════════
"""

class Reservation:
    """
    [IDEE] AGGREGATE : Réservation de livre
    
    Permet de réserver livre quand aucun exemplaire disponible.
    Expire après 3 jours si pas récupéré.
    
    Attributes:
        reservation_id: ID unique
        member: Membre ayant réservé
        book: Livre réservé
        reserved_date: Date de réservation
        expiry_date: Date d'expiration
        status: Statut
    """
    
    # Configuration
    RESERVATION_EXPIRY_DAYS = 3
    
    def __init__(
        self,
        reservation_id: str,
        member: Member,
        book: Book,
        reserved_date: date = None
    ):
        """
        Initialise réservation.
        
        Args:
            reservation_id: ID unique
            member: Membre
            book: Livre réservé
            reserved_date: Date de réservation
        """
        # Identité
        self._reservation_id = reservation_id
        
        # Références
        self._member = member
        self._book = book
        
        # Dates
        self._reserved_date = reserved_date or date.today()
        self._expiry_date = self._reserved_date + timedelta(
            days=self.RESERVATION_EXPIRY_DAYS
        )
        
        # État
        self._status = ReservationStatus.PENDING
        self._notified_date: Optional[date] = None
    
    # ========================================================================
    # PROPERTIES
    # ========================================================================
    
    @property
    def reservation_id(self) -> str:
        """ID réservation."""
        return self._reservation_id
    
    @property
    def member(self) -> Member:
        """Membre."""
        return self._member
    
    @property
    def book(self) -> Book:
        """Livre réservé."""
        return self._book
    
    @property
    def reserved_date(self) -> date:
        """Date de réservation."""
        return self._reserved_date
    
    @property
    def expiry_date(self) -> date:
        """Date d'expiration."""
        return self._expiry_date
    
    @property
    def status(self) -> ReservationStatus:
        """Statut."""
        return self._status
    
    # ========================================================================
    # BUSINESS METHODS
    # ========================================================================
    
    def notify_ready(self, notification_date: date = None) -> None:
        """
        [IDEE] COMMAND : Notifier que livre est disponible
        
        Args:
            notification_date: Date de notification
        """
        if self._status != ReservationStatus.PENDING:
            raise ValueError(
                f"Cannot notify reservation in {self._status.value} status"
            )
        
        self._status = ReservationStatus.READY
        self._notified_date = notification_date or date.today()
        
        # Nouvelle expiration : 3 jours après notification
        self._expiry_date = self._notified_date + timedelta(
            days=self.RESERVATION_EXPIRY_DAYS
        )
    
    def fulfill(self) -> None:
        """
        [IDEE] COMMAND : Marquer comme satisfaite (livre emprunté)
        """
        if self._status not in [ReservationStatus.PENDING, ReservationStatus.READY]:
            raise ValueError(
                f"Cannot fulfill reservation in {self._status.value} status"
            )
        
        self._status = ReservationStatus.FULFILLED
    
    def cancel(self) -> None:
        """
        [IDEE] COMMAND : Annuler réservation
        """
        if self._status in [ReservationStatus.FULFILLED, ReservationStatus.EXPIRED]:
            raise ValueError(
                f"Cannot cancel reservation in {self._status.value} status"
            )
        
        self._status = ReservationStatus.CANCELLED
    
    def check_expiry(self, check_date: date = None) -> None:
        """
        Vérifier si réservation expirée.
        
        Args:
            check_date: Date de vérification
        """
        check_date = check_date or date.today()
        
        if self._status in [ReservationStatus.PENDING, ReservationStatus.READY]:
            if check_date > self._expiry_date:
                self._status = ReservationStatus.EXPIRED
    
    # ========================================================================
    # QUERIES
    # ========================================================================
    
    def is_active(self) -> bool:
        """Vérifier si réservation active."""
        return self._status in [ReservationStatus.PENDING, ReservationStatus.READY]
    
    def is_expired(self, check_date: date = None) -> bool:
        """Vérifier si expirée."""
        self.check_expiry(check_date)
        return self._status == ReservationStatus.EXPIRED
    
    def days_until_expiry(self, check_date: date = None) -> int:
        """
        Jours restants avant expiration.
        
        Returns:
            Nombre de jours (négatif si expiré)
        """
        check_date = check_date or date.today()
        return (self._expiry_date - check_date).days
    
    # ========================================================================
    # REPRÉSENTATION
    # ========================================================================
    
    def __eq__(self, other) -> bool:
        """Égalité basée sur reservation_id."""
        if not isinstance(other, Reservation):
            return False
        return self._reservation_id == other._reservation_id
    
    def __hash__(self) -> int:
        """Hash basé sur reservation_id."""
        return hash(self._reservation_id)
    
    def __repr__(self) -> str:
        return (
            f"Reservation(id={self._reservation_id}, "
            f"book={self._book.title}, status={self._status.value})"
        )
    
    def __str__(self) -> str:
        return (
            f"Reservation {self._reservation_id}\n"
            f"  Book: {self._book.title}\n"
            f"  Member: {self._member.name}\n"
            f"  Status: {self._status.value}\n"
            f"  Expires: {self._expiry_date}"
        )


# Ce fichier continue avec la Partie 3...
# ============================================================================

# ============================================================================
# [DOCS] PROJET PRATIQUE 3 : SYSTÈME DE BIBLIOTHÈQUE (PARTIE 3)
# ============================================================================
#
# Suite du projet : Domain Services, Specifications, Business Rules
#
# [OBJECTIF] CONTENU :
# - Domain Services (LoanPolicy, FineCalculator)
# - Specification Pattern (requêtes complexes)
# - Business Rules explicites
# - Policy Pattern
# - Tests TDD complets
# - Domain Events
#
# ============================================================================


# Suite des Parties 1 et 2...

from typing import List, Optional, Callable, Protocol
from datetime import datetime, date, timedelta
from decimal import Decimal
from abc import ABC, abstractmethod
from dataclasses import dataclass


"""
═══════════════════════════════════════════════════════════════════════════
DOMAIN SERVICES
═══════════════════════════════════════════════════════════════════════════

[IDEE] DOMAIN SERVICE

Logique métier qui ne rentre pas naturellement dans une entité.
Opérations impliquant plusieurs aggregates ou calculs complexes.


Quand utiliser Domain Service ?
- Logique métier sans "maison naturelle"
- Coordination entre plusieurs aggregates
- Calculs complexes basés sur business rules
- Policies métier
"""


# ============================================================================
# DOMAIN SERVICE : Loan Policy
# ============================================================================

class LoanPolicy:
    """
    [IDEE] DOMAIN SERVICE : Politique de prêt
    
    Centralise toutes les règles métier liées aux prêts.
    Séparation claire des business rules.
    
    Responsibilities:
    - Déterminer durée de prêt selon type membre
    - Vérifier si membre peut emprunter
    - Calculer limite d'emprunts
    - Valider prêt selon contexte
    """
    
    # Configuration centralisée
    MAX_LOANS_CONFIG = {
        MembershipType.STANDARD: 3,
        MembershipType.PREMIUM: 10,
        MembershipType.STUDENT: 5
    }
    
    LOAN_DURATION_CONFIG = {
        MembershipType.STANDARD: 14,  # jours
        MembershipType.PREMIUM: 21,
        MembershipType.STUDENT: 7
    }
    
    MAX_FINE_THRESHOLD = Money(Decimal("10.00"), "EUR")
    
    def can_member_borrow(self, member: Member) -> tuple[bool, Optional[str]]:
        """
        [IDEE] BUSINESS RULE : Vérifier si membre peut emprunter
        
        Centralise TOUTES les vérifications.
        
        Args:
            member: Membre à vérifier
            
        Returns:
            (peut_emprunter, raison_refus)
        """
        # Règle 1: Adhésion expirée
        if member.is_membership_expired():
            return False, "Membership expired"
        
        # Règle 2: Statut membre
        if member.status != MemberStatus.ACTIVE:
            return False, f"Member status is {member.status.value}"
        
        # Règle 3: Amendes excessives
        total_fines = member.total_unpaid_fines()
        if total_fines > self.MAX_FINE_THRESHOLD:
            return False, f"Unpaid fines ({total_fines}) exceed limit ({self.MAX_FINE_THRESHOLD})"
        
        # Règle 4: Limite d'emprunts
        max_loans = self.get_max_loans(member.membership_type)
        if member.active_loan_count() >= max_loans:
            return False, f"Maximum loans reached ({max_loans})"
        
        # Règle 5: Prêts en retard
        if member.has_overdue_loans():
            return False, "Has overdue loans"
        
        return True, None
    
    def get_loan_duration(self, membership_type: MembershipType) -> int:
        """
        Obtenir durée de prêt en jours.
        
        Args:
            membership_type: Type d'adhésion
            
        Returns:
            Nombre de jours
        """
        return self.LOAN_DURATION_CONFIG[membership_type]
    
    def get_max_loans(self, membership_type: MembershipType) -> int:
        """
        Obtenir limite d'emprunts.
        
        Args:
            membership_type: Type d'adhésion
            
        Returns:
            Nombre maximum de prêts simultanés
        """
        return self.MAX_LOANS_CONFIG[membership_type]
    
    def calculate_due_date(
        self,
        membership_type: MembershipType,
        loan_date: date = None
    ) -> date:
        """
        Calculer date limite de retour.
        
        Args:
            membership_type: Type d'adhésion
            loan_date: Date d'emprunt (aujourd'hui par défaut)
            
        Returns:
            Date limite
        """
        loan_date = loan_date or date.today()
        duration = self.get_loan_duration(membership_type)
        return loan_date + timedelta(days=duration)
    
    def can_renew_loan(self, loan: Loan) -> tuple[bool, Optional[str]]:
        """
        [IDEE] BUSINESS RULE : Vérifier si prêt peut être renouvelé
        
        Args:
            loan: Prêt à renouveler
            
        Returns:
            (peut_renouveler, raison_refus)
        """
        # Ne peut pas renouveler si déjà retourné
        if loan.status == LoanStatus.RETURNED:
            return False, "Loan already returned"
        
        # Ne peut pas renouveler si en retard
        if loan.is_overdue():
            return False, "Loan is overdue"
        
        # Vérifier si livre réservé par quelqu'un d'autre
        # (cette logique nécessiterait accès à repository - voir Use Case)
        
        return True, None


# ============================================================================
# DOMAIN SERVICE : Fine Calculator
# ============================================================================

class FineCalculator:
    """
    [IDEE] DOMAIN SERVICE : Calculateur d'amendes
    
    Centralise tous les calculs d'amendes.
    
    Business Rules:
    - Retard: 0.50€ par jour
    - Livre endommagé: 20€ forfait
    - Livre perdu: Prix du livre + 10€ frais
    """
    
    # Configuration
    FINE_PER_DAY_OVERDUE = Money(Decimal("0.50"), "EUR")
    DAMAGED_BOOK_FINE = Money(Decimal("20.00"), "EUR")
    LOST_BOOK_PROCESSING_FEE = Money(Decimal("10.00"), "EUR")
    
    def calculate_overdue_fine(self, days_overdue: int) -> Money:
        """
        [IDEE] CALCULATION : Amende pour retard
        
        Args:
            days_overdue: Nombre de jours de retard
            
        Returns:
            Montant de l'amende
        """
        if days_overdue <= 0:
            return Money.zero()
        
        return self.FINE_PER_DAY_OVERDUE * days_overdue
    
    def calculate_damaged_book_fine(self) -> Money:
        """
        [IDEE] CALCULATION : Amende livre endommagé
        
        Returns:
            Montant forfaitaire
        """
        return self.DAMAGED_BOOK_FINE
    
    def calculate_lost_book_fine(self, book_price: Money) -> Money:
        """
        [IDEE] CALCULATION : Amende livre perdu
        
        Args:
            book_price: Prix du livre
            
        Returns:
            Prix livre + frais de traitement
        """
        return book_price + self.LOST_BOOK_PROCESSING_FEE
    
    def project_future_fine(
        self,
        loan: Loan,
        projection_date: date
    ) -> Money:
        """
        Projeter amende future.
        
        Utile pour avertir membres du coût si pas retour.
        
        Args:
            loan: Prêt en cours
            projection_date: Date de projection
            
        Returns:
            Amende projetée
        """
        if projection_date <= loan.due_date:
            return Money.zero()
        
        days_overdue = (projection_date - loan.due_date).days
        return self.calculate_overdue_fine(days_overdue)


"""
═══════════════════════════════════════════════════════════════════════════
SPECIFICATION PATTERN
═══════════════════════════════════════════════════════════════════════════

[IDEE] SPECIFICATION PATTERN

Encapsule business rules de sélection.
Requêtes complexes réutilisables et composables.


Avantages:
[OK] Business rules explicites et nommées
[OK] Réutilisables
[OK] Composables (AND, OR, NOT)
[OK] Testables unitairement
"""

class Specification(ABC):
    """
    [IDEE] SPECIFICATION (Pattern)
    
    Prédicat métier encapsulé.
    """
    
    @abstractmethod
    def is_satisfied_by(self, candidate) -> bool:
        """
        Vérifier si candidat satisfait spécification.
        
        Args:
            candidate: Objet à vérifier
            
        Returns:
            True si satisfait
        """
        pass
    
    def and_specification(self, other: 'Specification') -> 'AndSpecification':
        """Combiner avec AND."""
        return AndSpecification(self, other)
    
    def or_specification(self, other: 'Specification') -> 'OrSpecification':
        """Combiner avec OR."""
        return OrSpecification(self, other)
    
    def not_specification(self) -> 'NotSpecification':
        """Inverser."""
        return NotSpecification(self)


class AndSpecification(Specification):
    """Spécification AND (composite)."""
    
    def __init__(self, spec1: Specification, spec2: Specification):
        self.spec1 = spec1
        self.spec2 = spec2
    
    def is_satisfied_by(self, candidate) -> bool:
        return (
            self.spec1.is_satisfied_by(candidate) and
            self.spec2.is_satisfied_by(candidate)
        )


class OrSpecification(Specification):
    """Spécification OR (composite)."""
    
    def __init__(self, spec1: Specification, spec2: Specification):
        self.spec1 = spec1
        self.spec2 = spec2
    
    def is_satisfied_by(self, candidate) -> bool:
        return (
            self.spec1.is_satisfied_by(candidate) or
            self.spec2.is_satisfied_by(candidate)
        )


class NotSpecification(Specification):
    """Spécification NOT."""
    
    def __init__(self, spec: Specification):
        self.spec = spec
    
    def is_satisfied_by(self, candidate) -> bool:
        return not self.spec.is_satisfied_by(candidate)


# ============================================================================
# BOOK SPECIFICATIONS
# ============================================================================

class AvailableBookSpecification(Specification):
    """
    [IDEE] SPEC : Livre avec au moins un exemplaire disponible
    """
    
    def is_satisfied_by(self, book: Book) -> bool:
        return book.has_available_copy()


class BookByCategorySpecification(Specification):
    """
    [IDEE] SPEC : Livre d'une catégorie spécifique
    """
    
    def __init__(self, category: str):
        self.category = category
    
    def is_satisfied_by(self, book: Book) -> bool:
        return book.category.lower() == self.category.lower()


class BookByAuthorSpecification(Specification):
    """
    [IDEE] SPEC : Livre d'un auteur spécifique
    """
    
    def __init__(self, author: str):
        self.author = author.lower()
    
    def is_satisfied_by(self, book: Book) -> bool:
        return self.author in book.author.lower()


class BookPublishedAfterSpecification(Specification):
    """
    [IDEE] SPEC : Livre publié après une certaine année
    """
    
    def __init__(self, year: int):
        self.year = year
    
    def is_satisfied_by(self, book: Book) -> bool:
        if book.publication_year is None:
            return False
        return book.publication_year > self.year


class PopularBookSpecification(Specification):
    """
    [IDEE] SPEC : Livre populaire (beaucoup d'exemplaires)
    
    Considéré populaire si >= 5 exemplaires.
    """
    
    POPULAR_THRESHOLD = 5
    
    def is_satisfied_by(self, book: Book) -> bool:
        return book.total_copies() >= self.POPULAR_THRESHOLD


# ============================================================================
# MEMBER SPECIFICATIONS
# ============================================================================

class ActiveMemberSpecification(Specification):
    """
    [IDEE] SPEC : Membre actif
    """
    
    def is_satisfied_by(self, member: Member) -> bool:
        return member.status == MemberStatus.ACTIVE


class MemberCanBorrowSpecification(Specification):
    """
    [IDEE] SPEC : Membre peut emprunter
    
    Utilise LoanPolicy pour vérification.
    """
    
    def __init__(self, loan_policy: LoanPolicy):
        self.loan_policy = loan_policy
    
    def is_satisfied_by(self, member: Member) -> bool:
        can_borrow, _ = self.loan_policy.can_member_borrow(member)
        return can_borrow


class MemberWithOverdueLoansSpecification(Specification):
    """
    [IDEE] SPEC : Membre avec prêts en retard
    """
    
    def is_satisfied_by(self, member: Member) -> bool:
        return member.has_overdue_loans()


class MemberWithUnpaidFinesSpecification(Specification):
    """
    [IDEE] SPEC : Membre avec amendes impayées
    """
    
    def is_satisfied_by(self, member: Member) -> bool:
        return member.total_unpaid_fines() > Money.zero()


class PremiumMemberSpecification(Specification):
    """
    [IDEE] SPEC : Membre premium
    """
    
    def is_satisfied_by(self, member: Member) -> bool:
        return member.membership_type == MembershipType.PREMIUM


# ============================================================================
# EXEMPLES D'UTILISATION SPECIFICATIONS
# ============================================================================

def example_specifications():
    """
    Exemples d'utilisation du Specification Pattern.
    """
    
    # Spécification simple
    available_spec = AvailableBookSpecification()
    
    # Spécification composée (AND)
    fiction_and_available = (
        BookByCategorySpecification("Fiction")
        .and_specification(AvailableBookSpecification())
    )
    
    # Spécification complexe (AND + OR)
    recent_fiction_or_classics = (
        BookByCategorySpecification("Fiction")
        .and_specification(BookPublishedAfterSpecification(2010))
        .or_specification(BookByCategorySpecification("Classic"))
    )
    
    # Spécification NOT
    not_science_fiction = (
        BookByCategorySpecification("Science Fiction")
        .not_specification()
    )
    
    # Utilisation pour filtrer
    all_books: List[Book] = []  # Récupérés depuis repository
    
    # Filtrer livres disponibles
    available_books = [
        book for book in all_books
        if available_spec.is_satisfied_by(book)
    ]
    
    # Filtrer avec spec composée
    fiction_available = [
        book for book in all_books
        if fiction_and_available.is_satisfied_by(book)
    ]


"""
═══════════════════════════════════════════════════════════════════════════
DOMAIN EVENTS
═══════════════════════════════════════════════════════════════════════════

[IDEE] DOMAIN EVENTS

Représentent faits métier importants.
Permettent découplage et réaction asynchrone.
"""

@dataclass(frozen=True)
class DomainEvent:
    """Événement domaine de base."""
    event_id: str
    occurred_at: datetime
    
    def __post_init__(self):
        if not self.event_id:
            import uuid
            object.__setattr__(self, 'event_id', str(uuid.uuid4()))
        
        if not self.occurred_at:
            object.__setattr__(self, 'occurred_at', datetime.now())


@dataclass(frozen=True)
class BookBorrowedEvent(DomainEvent):
    """Événement : Livre emprunté."""
    member_id: MemberId
    book_isbn: ISBN
    loan_id: str
    due_date: date


@dataclass(frozen=True)
class BookReturnedEvent(DomainEvent):
    """Événement : Livre retourné."""
    member_id: MemberId
    book_isbn: ISBN
    loan_id: str
    return_date: date
    fine_amount: Optional[Money] = None


@dataclass(frozen=True)
class BookReservedEvent(DomainEvent):
    """Événement : Livre réservé."""
    member_id: MemberId
    book_isbn: ISBN
    reservation_id: str


@dataclass(frozen=True)
class LoanOverdueEvent(DomainEvent):
    """Événement : Prêt en retard."""
    member_id: MemberId
    loan_id: str
    days_overdue: int
    projected_fine: Money


@dataclass(frozen=True)
class MemberSuspendedEvent(DomainEvent):
    """Événement : Membre suspendu."""
    member_id: MemberId
    reason: str
    unpaid_fines: Money


@dataclass(frozen=True)
class ReservationReadyEvent(DomainEvent):
    """Événement : Réservation prête (livre disponible)."""
    member_id: MemberId
    reservation_id: str
    book_isbn: ISBN


"""
═══════════════════════════════════════════════════════════════════════════
BUSINESS RULES EXPLICITES
═══════════════════════════════════════════════════════════════════════════

[IDEE] BUSINESS RULES AS FIRST-CLASS CITIZENS

Rendre rules explicites et testables.
"""

class BusinessRule(ABC):
    """
    [IDEE] BUSINESS RULE
    
    Règle métier explicite et testable.
    """
    
    @abstractmethod
    def is_satisfied(self) -> bool:
        """Vérifier si règle satisfaite."""
        pass
    
    @abstractmethod
    def get_error_message(self) -> str:
        """Message d'erreur si règle violée."""
        pass
    
    def check(self) -> None:
        """
        Vérifier règle et lever exception si violée.
        
        Raises:
            ValueError: Si règle violée
        """
        if not self.is_satisfied():
            raise ValueError(self.get_error_message())


class MemberCanBorrowRule(BusinessRule):
    """
    [IDEE] RULE : Membre peut emprunter
    """
    
    def __init__(self, member: Member, loan_policy: LoanPolicy):
        self.member = member
        self.loan_policy = loan_policy
    
    def is_satisfied(self) -> bool:
        can_borrow, _ = self.loan_policy.can_member_borrow(self.member)
        return can_borrow
    
    def get_error_message(self) -> str:
        _, reason = self.loan_policy.can_member_borrow(self.member)
        return f"Member cannot borrow: {reason}"


class BookCopyAvailableRule(BusinessRule):
    """
    [IDEE] RULE : Exemplaire disponible
    """
    
    def __init__(self, book_copy: BookCopy):
        self.book_copy = book_copy
    
    def is_satisfied(self) -> bool:
        return self.book_copy.is_available()
    
    def get_error_message(self) -> str:
        return f"Book copy {self.book_copy.copy_id} is not available"


class NoOverdueLoansRule(BusinessRule):
    """
    [IDEE] RULE : Pas de prêts en retard
    """
    
    def __init__(self, member: Member):
        self.member = member
    
    def is_satisfied(self) -> bool:
        return not self.member.has_overdue_loans()
    
    def get_error_message(self) -> str:
        overdue_count = len(self.member.get_overdue_loans())
        return f"Member has {overdue_count} overdue loan(s)"


class UnpaidFinesWithinLimitRule(BusinessRule):
    """
    [IDEE] RULE : Amendes dans la limite
    """
    
    def __init__(self, member: Member, max_amount: Money):
        self.member = member
        self.max_amount = max_amount
    
    def is_satisfied(self) -> bool:
        return self.member.total_unpaid_fines() <= self.max_amount
    
    def get_error_message(self) -> str:
        total = self.member.total_unpaid_fines()
        return f"Unpaid fines ({total}) exceed limit ({self.max_amount})"


"""
═══════════════════════════════════════════════════════════════════════════
TESTS TDD (Test Driven Development)
═══════════════════════════════════════════════════════════════════════════
"""

import pytest


class TestLoanPolicy:
    """Tests TDD pour LoanPolicy."""
    
    @pytest.fixture
    def loan_policy(self):
        """Fixture loan policy."""
        return LoanPolicy()
    
    @pytest.fixture
    def active_member(self):
        """Fixture membre actif."""
        member = Member(
            member_id=MemberId.generate(),
            name="Alice Test",
            email=EmailAddress("alice@test.com"),
            membership_type=MembershipType.STANDARD
        )
        return member
    
    def test_active_member_can_borrow(self, loan_policy, active_member):
        """Test membre actif peut emprunter."""
        can_borrow, reason = loan_policy.can_member_borrow(active_member)
        
        assert can_borrow is True
        assert reason is None
    
    def test_member_with_excessive_fines_cannot_borrow(
        self,
        loan_policy,
        active_member
    ):
        """Test membre avec amendes excessives ne peut pas emprunter."""
        # Ajouter amende excessive
        fine = Fine(
            fine_id="FINE-001",
            member=active_member,
            amount=Money(Decimal("15.00"), "EUR"),
            reason="Test"
        )
        active_member._fines.append(fine)
        
        can_borrow, reason = loan_policy.can_member_borrow(active_member)
        
        assert can_borrow is False
        assert "fines" in reason.lower()
    
    def test_get_loan_duration_standard(self, loan_policy):
        """Test durée prêt membre standard."""
        duration = loan_policy.get_loan_duration(MembershipType.STANDARD)
        assert duration == 14
    
    def test_get_loan_duration_premium(self, loan_policy):
        """Test durée prêt membre premium."""
        duration = loan_policy.get_loan_duration(MembershipType.PREMIUM)
        assert duration == 21
    
    def test_calculate_due_date(self, loan_policy):
        """Test calcul date limite."""
        loan_date = date(2024, 1, 1)
        due_date = loan_policy.calculate_due_date(
            MembershipType.STANDARD,
            loan_date
        )
        
        assert due_date == date(2024, 1, 15)  # +14 jours


class TestFineCalculator:
    """Tests TDD pour FineCalculator."""
    
    @pytest.fixture
    def calculator(self):
        """Fixture calculator."""
        return FineCalculator()
    
    def test_calculate_overdue_fine_no_days(self, calculator):
        """Test amende 0 jours de retard."""
        fine = calculator.calculate_overdue_fine(0)
        assert fine.is_zero()
    
    def test_calculate_overdue_fine_one_day(self, calculator):
        """Test amende 1 jour de retard."""
        fine = calculator.calculate_overdue_fine(1)
        assert fine == Money(Decimal("0.50"), "EUR")
    
    def test_calculate_overdue_fine_multiple_days(self, calculator):
        """Test amende plusieurs jours."""
        fine = calculator.calculate_overdue_fine(10)
        assert fine == Money(Decimal("5.00"), "EUR")
    
    def test_calculate_damaged_book_fine(self, calculator):
        """Test amende livre endommagé."""
        fine = calculator.calculate_damaged_book_fine()
        assert fine == Money(Decimal("20.00"), "EUR")
    
    def test_calculate_lost_book_fine(self, calculator):
        """Test amende livre perdu."""
        book_price = Money(Decimal("25.00"), "EUR")
        fine = calculator.calculate_lost_book_fine(book_price)
        
        # 25 + 10 = 35
        assert fine == Money(Decimal("35.00"), "EUR")


class TestSpecifications:
    """Tests TDD pour Specifications."""
    
    @pytest.fixture
    def available_book(self):
        """Fixture livre disponible."""
        book = Book(
            isbn=ISBN.from_digits("9783161484100"),
            title="Test Book",
            author="Test Author",
            category="Fiction"
        )
        
        # Ajouter exemplaire disponible
        copy = BookCopy("COPY-001", book)
        book.add_copy(copy)
        
        return book
    
    def test_available_book_specification(self, available_book):
        """Test spec livre disponible."""
        spec = AvailableBookSpecification()
        
        assert spec.is_satisfied_by(available_book) is True
    
    def test_book_by_category_specification(self, available_book):
        """Test spec catégorie."""
        spec = BookByCategorySpecification("Fiction")
        
        assert spec.is_satisfied_by(available_book) is True
    
    def test_and_specification(self, available_book):
        """Test composition AND."""
        spec = (
            AvailableBookSpecification()
            .and_specification(BookByCategorySpecification("Fiction"))
        )
        
        assert spec.is_satisfied_by(available_book) is True
    
    def test_or_specification(self, available_book):
        """Test composition OR."""
        spec = (
            BookByCategorySpecification("Science")
            .or_specification(BookByCategorySpecification("Fiction"))
        )
        
        assert spec.is_satisfied_by(available_book) is True
    
    def test_not_specification(self, available_book):
        """Test NOT."""
        spec = BookByCategorySpecification("Science").not_specification()
        
        assert spec.is_satisfied_by(available_book) is True


class TestBusinessRules:
    """Tests TDD pour Business Rules."""
    
    @pytest.fixture
    def member(self):
        """Fixture membre."""
        return Member(
            member_id=MemberId.generate(),
            name="Bob Test",
            email=EmailAddress("bob@test.com"),
            membership_type=MembershipType.STANDARD
        )
    
    @pytest.fixture
    def available_copy(self):
        """Fixture exemplaire disponible."""
        book = Book(
            isbn=ISBN.from_digits("9783161484100"),
            title="Test",
            author="Author"
        )
        return BookCopy("COPY-001", book)
    
    def test_member_can_borrow_rule_satisfied(self, member):
        """Test règle satisfaite."""
        policy = LoanPolicy()
        rule = MemberCanBorrowRule(member, policy)
        
        assert rule.is_satisfied() is True
        # Ne doit pas lever exception
        rule.check()
    
    def test_book_copy_available_rule_satisfied(self, available_copy):
        """Test règle exemplaire disponible."""
        rule = BookCopyAvailableRule(available_copy)
        
        assert rule.is_satisfied() is True
        rule.check()
    
    def test_book_copy_available_rule_violated(self, available_copy):
        """Test règle exemplaire violée."""
        available_copy.borrow()  # Emprunter
        
        rule = BookCopyAvailableRule(available_copy)
        
        assert rule.is_satisfied() is False
        
        with pytest.raises(ValueError, match="not available"):
            rule.check()


# Ce fichier continue avec la Partie 4...
# ============================================================================

# ============================================================================
# [DOCS] PROJET PRATIQUE 3 : SYSTÈME DE BIBLIOTHÈQUE (PARTIE 4 - FINALE)
# ============================================================================
#
# Fin du projet : Application Layer, Infrastructure, Démonstration
#
# [OBJECTIF] CONTENU :
# - Use Cases (Application Layer)
# - Ports (interfaces)
# - Adapters (implementations)
# - Repositories
# - Application complète
# - CLI
# - Démonstration DDD complète
# - Récapitulatif final
#
# ============================================================================


# Suite de la Partie 3...

from typing import List, Optional, Protocol, Dict
from abc import abstractmethod
from datetime import date
import uuid


"""
═══════════════════════════════════════════════════════════════════════════
PORTS (Interfaces - Dependency Inversion)
═══════════════════════════════════════════════════════════════════════════

[IDEE] PORTS

Interfaces définissant contrats.
Application dépend de ports, pas d'implémentations concrètes.
"""

# ============================================================================
# REPOSITORY PORTS
# ============================================================================

class BookRepository(Protocol):
    """Port : Repository livres."""
    
    @abstractmethod
    def save(self, book: Book) -> None:
        """Sauvegarder livre."""
        ...
    
    @abstractmethod
    def find_by_isbn(self, isbn: ISBN) -> Optional[Book]:
        """Trouver par ISBN."""
        ...
    
    @abstractmethod
    def find_all(self) -> List[Book]:
        """Tous les livres."""
        ...
    
    @abstractmethod
    def find_by_specification(self, spec: Specification) -> List[Book]:
        """Trouver selon spécification."""
        ...


class MemberRepository(Protocol):
    """Port : Repository membres."""
    
    @abstractmethod
    def save(self, member: Member) -> None:
        """Sauvegarder membre."""
        ...
    
    @abstractmethod
    def find_by_id(self, member_id: MemberId) -> Optional[Member]:
        """Trouver par ID."""
        ...
    
    @abstractmethod
    def find_all(self) -> List[Member]:
        """Tous les membres."""
        ...


"""
═══════════════════════════════════════════════════════════════════════════
USE CASES (Application Layer)
═══════════════════════════════════════════════════════════════════════════

[IDEE] USE CASE

Représente intention utilisateur.
Orchestre domaine et infrastructure.
"""

# ============================================================================
# USE CASE : Borrow Book
# ============================================================================

@dataclass
class BorrowBookCommand:
    """Command pour emprunter livre."""
    member_id: str
    book_isbn: str
    copy_id: str


@dataclass
class BorrowBookResult:
    """Résultat emprunt."""
    success: bool
    loan_id: Optional[str] = None
    due_date: Optional[date] = None
    error_message: Optional[str] = None


class BorrowBookUseCase:
    """
    [IDEE] USE CASE : Emprunter un livre
    
    Coordonne :
    - Validation via LoanPolicy
    - Création Loan
    - Mise à jour repositories
    - Publication événements
    """
    
    def __init__(
        self,
        member_repo: MemberRepository,
        book_repo: BookRepository,
        loan_policy: LoanPolicy
    ):
        """
        Initialise use case.
        
        Args:
            member_repo: Repository membres (DIP)
            book_repo: Repository livres (DIP)
            loan_policy: Politique prêt (Domain Service)
        """
        self.member_repo = member_repo
        self.book_repo = book_repo
        self.loan_policy = loan_policy
    
    def execute(self, command: BorrowBookCommand) -> BorrowBookResult:
        """
        Exécuter use case.
        
        Args:
            command: Commande
            
        Returns:
            Résultat
        """
        try:
            # 1. Récupérer membre
            member_id = MemberId(command.member_id)
            member = self.member_repo.find_by_id(member_id)
            
            if not member:
                return BorrowBookResult(
                    success=False,
                    error_message=f"Member {command.member_id} not found"
                )
            
            # 2. Récupérer livre
            isbn = ISBN(command.book_isbn)
            book = self.book_repo.find_by_isbn(isbn)
            
            if not book:
                return BorrowBookResult(
                    success=False,
                    error_message=f"Book {command.book_isbn} not found"
                )
            
            # 3. Trouver exemplaire
            book_copy = None
            for copy in book.copies:
                if copy.copy_id == command.copy_id:
                    book_copy = copy
                    break
            
            if not book_copy:
                return BorrowBookResult(
                    success=False,
                    error_message=f"Copy {command.copy_id} not found"
                )
            
            # 4. Vérifier business rules via domain service
            can_borrow, reason = self.loan_policy.can_member_borrow(member)
            if not can_borrow:
                return BorrowBookResult(
                    success=False,
                    error_message=reason
                )
            
            # 5. Emprunter (aggregate gère invariants)
            loan = member.borrow_book(book_copy)
            
            # 6. Sauvegarder (Unit of Work pattern recommandé)
            self.member_repo.save(member)
            self.book_repo.save(book)
            
            # 7. Publier événement (si event publisher configuré)
            # event = BookBorrowedEvent(...)
            # self.event_publisher.publish(event)
            
            return BorrowBookResult(
                success=True,
                loan_id=loan.loan_id,
                due_date=loan.due_date
            )
            
        except Exception as e:
            return BorrowBookResult(
                success=False,
                error_message=str(e)
            )


# ============================================================================
# USE CASE : Return Book
# ============================================================================

@dataclass
class ReturnBookCommand:
    """Command pour retourner livre."""
    member_id: str
    loan_id: str
    return_date: Optional[date] = None


@dataclass
class ReturnBookResult:
    """Résultat retour."""
    success: bool
    fine_amount: Optional[Money] = None
    error_message: Optional[str] = None


class ReturnBookUseCase:
    """
    [IDEE] USE CASE : Retourner un livre
    """
    
    def __init__(
        self,
        member_repo: MemberRepository,
        book_repo: BookRepository
    ):
        self.member_repo = member_repo
        self.book_repo = book_repo
    
    def execute(self, command: ReturnBookCommand) -> ReturnBookResult:
        """Exécuter retour."""
        try:
            # Récupérer membre
            member_id = MemberId(command.member_id)
            member = self.member_repo.find_by_id(member_id)
            
            if not member:
                return ReturnBookResult(
                    success=False,
                    error_message="Member not found"
                )
            
            # Trouver prêt
            loan = None
            for active_loan in member.active_loans:
                if active_loan.loan_id == command.loan_id:
                    loan = active_loan
                    break
            
            if not loan:
                return ReturnBookResult(
                    success=False,
                    error_message="Loan not found"
                )
            
            # Retourner (aggregate calcule amende si besoin)
            fine = member.return_book(loan, command.return_date)
            
            # Sauvegarder
            self.member_repo.save(member)
            
            return ReturnBookResult(
                success=True,
                fine_amount=fine.amount if fine else None
            )
            
        except Exception as e:
            return ReturnBookResult(
                success=False,
                error_message=str(e)
            )


# ============================================================================
# USE CASE : Reserve Book
# ============================================================================

@dataclass
class ReserveBookCommand:
    """Command pour réserver livre."""
    member_id: str
    book_isbn: str


@dataclass
class ReserveBookResult:
    """Résultat réservation."""
    success: bool
    reservation_id: Optional[str] = None
    error_message: Optional[str] = None


class ReserveBookUseCase:
    """
    [IDEE] USE CASE : Réserver un livre
    """
    
    def __init__(
        self,
        member_repo: MemberRepository,
        book_repo: BookRepository
    ):
        self.member_repo = member_repo
        self.book_repo = book_repo
    
    def execute(self, command: ReserveBookCommand) -> ReserveBookResult:
        """Exécuter réservation."""
        try:
            # Récupérer membre
            member = self.member_repo.find_by_id(MemberId(command.member_id))
            if not member:
                return ReserveBookResult(
                    success=False,
                    error_message="Member not found"
                )
            
            # Récupérer livre
            book = self.book_repo.find_by_isbn(ISBN(command.book_isbn))
            if not book:
                return ReserveBookResult(
                    success=False,
                    error_message="Book not found"
                )
            
            # Réserver (aggregate valide business rules)
            reservation = member.reserve_book(book)
            
            # Sauvegarder
            self.member_repo.save(member)
            
            return ReserveBookResult(
                success=True,
                reservation_id=reservation.reservation_id
            )
            
        except Exception as e:
            return ReserveBookResult(
                success=False,
                error_message=str(e)
            )


"""
═══════════════════════════════════════════════════════════════════════════
ADAPTERS (Infrastructure - Implementations)
═══════════════════════════════════════════════════════════════════════════
"""

# ============================================================================
# ADAPTER : In-Memory Repositories
# ============================================================================

class InMemoryBookRepository:
    """
    [IDEE] ADAPTER : Repository livres en mémoire
    
    Implémente BookRepository Protocol.
    """
    
    def __init__(self):
        """Initialise repository vide."""
        self._books: Dict[str, Book] = {}  # isbn -> Book
    
    def save(self, book: Book) -> None:
        """Sauvegarder livre."""
        self._books[str(book.isbn)] = book
    
    def find_by_isbn(self, isbn: ISBN) -> Optional[Book]:
        """Trouver par ISBN."""
        return self._books.get(str(isbn))
    
    def find_all(self) -> List[Book]:
        """Tous les livres."""
        return list(self._books.values())
    
    def find_by_specification(self, spec: Specification) -> List[Book]:
        """
        Trouver selon spécification.
        
        Args:
            spec: Spécification à appliquer
            
        Returns:
            Livres satisfaisant spécification
        """
        return [
            book for book in self._books.values()
            if spec.is_satisfied_by(book)
        ]


class InMemoryMemberRepository:
    """
    [IDEE] ADAPTER : Repository membres en mémoire
    """
    
    def __init__(self):
        """Initialise repository vide."""
        self._members: Dict[str, Member] = {}  # member_id -> Member
    
    def save(self, member: Member) -> None:
        """Sauvegarder membre."""
        self._members[str(member.member_id)] = member
    
    def find_by_id(self, member_id: MemberId) -> Optional[Member]:
        """Trouver par ID."""
        return self._members.get(str(member_id))
    
    def find_all(self) -> List[Member]:
        """Tous les membres."""
        return list(self._members.values())
    
    def find_by_specification(self, spec: Specification) -> List[Member]:
        """Trouver selon spécification."""
        return [
            member for member in self._members.values()
            if spec.is_satisfied_by(member)
        ]


"""
═══════════════════════════════════════════════════════════════════════════
APPLICATION SERVICE (Facade)
═══════════════════════════════════════════════════════════════════════════
"""

class LibraryService:
    """
    [IDEE] APPLICATION SERVICE
    
    Façade simplifiant accès aux use cases.
    """
    
    def __init__(
        self,
        member_repo: MemberRepository,
        book_repo: BookRepository,
        loan_policy: LoanPolicy,
        fine_calculator: FineCalculator
    ):
        """
        Initialise service.
        
        Args:
            member_repo: Repository membres
            book_repo: Repository livres
            loan_policy: Politique prêt
            fine_calculator: Calculateur amendes
        """
        self.member_repo = member_repo
        self.book_repo = book_repo
        self.loan_policy = loan_policy
        self.fine_calculator = fine_calculator
        
        # Use cases
        self.borrow_use_case = BorrowBookUseCase(
            member_repo, book_repo, loan_policy
        )
        self.return_use_case = ReturnBookUseCase(
            member_repo, book_repo
        )
        self.reserve_use_case = ReserveBookUseCase(
            member_repo, book_repo
        )
    
    # ========================================================================
    # Commands (Write)
    # ========================================================================
    
    def borrow_book(
        self,
        member_id: str,
        book_isbn: str,
        copy_id: str
    ) -> BorrowBookResult:
        """Emprunter livre."""
        command = BorrowBookCommand(member_id, book_isbn, copy_id)
        return self.borrow_use_case.execute(command)
    
    def return_book(
        self,
        member_id: str,
        loan_id: str,
        return_date: date = None
    ) -> ReturnBookResult:
        """Retourner livre."""
        command = ReturnBookCommand(member_id, loan_id, return_date)
        return self.return_use_case.execute(command)
    
    def reserve_book(
        self,
        member_id: str,
        book_isbn: str
    ) -> ReserveBookResult:
        """Réserver livre."""
        command = ReserveBookCommand(member_id, book_isbn)
        return self.reserve_use_case.execute(command)
    
    # ========================================================================
    # Queries (Read)
    # ========================================================================
    
    def get_member(self, member_id: str) -> Optional[Member]:
        """Obtenir membre."""
        return self.member_repo.find_by_id(MemberId(member_id))
    
    def get_book(self, isbn: str) -> Optional[Book]:
        """Obtenir livre."""
        return self.book_repo.find_by_isbn(ISBN(isbn))
    
    def find_available_books(self) -> List[Book]:
        """Livres disponibles."""
        spec = AvailableBookSpecification()
        return self.book_repo.find_by_specification(spec)
    
    def get_members_with_overdue_loans(self) -> List[Member]:
        """Membres avec retards."""
        spec = MemberWithOverdueLoansSpecification()
        return self.member_repo.find_by_specification(spec)


"""
═══════════════════════════════════════════════════════════════════════════
FACTORY : Application Setup
═══════════════════════════════════════════════════════════════════════════
"""

def create_library_application() -> LibraryService:
    """
    [IDEE] FACTORY : Créer application complète
    
    Assemble tous composants (DI Container simple).
    
    Returns:
        Service bibliothèque configuré
    """
    # Infrastructure
    book_repo = InMemoryBookRepository()
    member_repo = InMemoryMemberRepository()
    
    # Domain Services
    loan_policy = LoanPolicy()
    fine_calculator = FineCalculator()
    
    # Application Service
    library_service = LibraryService(
        member_repo,
        book_repo,
        loan_policy,
        fine_calculator
    )
    
    return library_service


"""
═══════════════════════════════════════════════════════════════════════════
DÉMONSTRATION COMPLÈTE
═══════════════════════════════════════════════════════════════════════════
"""

def demo_library_system():
    """
    Démonstration complète du système.
    
    Montre tous concepts DDD en action.
    """
    print("\n" + "="*80)
    print("[DOCS] LIBRARY SYSTEM - DDD & Clean Architecture Demo")
    print("="*80)
    
    # ========================================================================
    print("\n[NOTE] Setup: Creating application...")
    # ========================================================================
    
    app = create_library_application()
    print("[OK] Application created")
    
    # ========================================================================
    print("\n[DOCS] Step 1: Adding books to catalog...")
    # ========================================================================
    
    # Créer livres
    book1 = Book(
        isbn=ISBN.from_digits("9780132350884"),
        title="Clean Code",
        author="Robert C. Martin",
        category="Programming",
        publication_year=2008
    )
    
    book2 = Book(
        isbn=ISBN.from_digits("9780201633610"),
        title="Design Patterns",
        author="Gang of Four",
        category="Programming",
        publication_year=1994
    )
    
    # Ajouter exemplaires
    book1.add_copy(BookCopy("COPY-001", book1))
    book1.add_copy(BookCopy("COPY-002", book1))
    
    book2.add_copy(BookCopy("COPY-003", book2))
    
    # Sauvegarder
    app.book_repo.save(book1)
    app.book_repo.save(book2)
    
    print(f"[OK] Added {len(app.book_repo.find_all())} books")
    print(f"   - {book1.title} ({book1.total_copies()} copies)")
    print(f"   - {book2.title} ({book2.total_copies()} copies)")
    
    # ========================================================================
    print("\n[UTILISATEURS] Step 2: Registering members...")
    # ========================================================================
    
    # Créer membres
    alice = Member(
        member_id=MemberId("MEM-000001"),
        name="Alice Developer",
        email=EmailAddress("alice@dev.com"),
        membership_type=MembershipType.PREMIUM
    )
    
    bob = Member(
        member_id=MemberId("MEM-000002"),
        name="Bob Student",
        email=EmailAddress("bob@student.com"),
        membership_type=MembershipType.STUDENT
    )
    
    # Sauvegarder
    app.member_repo.save(alice)
    app.member_repo.save(bob)
    
    print(f"[OK] Registered {len(app.member_repo.find_all())} members")
    print(f"   - {alice.name} ({alice.membership_type.value})")
    print(f"   - {bob.name} ({bob.membership_type.value})")
    
    # ========================================================================
    print("\n[GUIDE] Step 3: Alice borrows 'Clean Code'...")
    # ========================================================================
    
    result = app.borrow_book(
        member_id=str(alice.member_id),
        book_isbn=str(book1.isbn),
        copy_id="COPY-001"
    )
    
    if result.success:
        print(f"[OK] Loan successful!")
        print(f"   Loan ID: {result.loan_id}")
        print(f"   Due date: {result.due_date}")
        print(f"   Duration: {app.loan_policy.get_loan_duration(alice.membership_type)} days (Premium)")
    else:
        print(f"[X] Loan failed: {result.error_message}")
    
    # ========================================================================
    print("\n[GUIDE] Step 4: Bob tries to borrow 'Design Patterns'...")
    # ========================================================================
    
    result2 = app.borrow_book(
        member_id=str(bob.member_id),
        book_isbn=str(book2.isbn),
        copy_id="COPY-003"
    )
    
    if result2.success:
        print(f"[OK] Loan successful!")
        print(f"   Loan ID: {result2.loan_id}")
        print(f"   Due date: {result2.due_date}")
        print(f"   Duration: {app.loan_policy.get_loan_duration(bob.membership_type)} days (Student)")
    
    # ========================================================================
    print("\n[GRAPHIQUE] Step 5: Checking catalog with Specifications...")
    # ========================================================================
    
    # Livres disponibles
    available = app.find_available_books()
    print(f"\n[DOCS] Available books: {len(available)}")
    for book in available:
        print(f"   - {book.title} ({book.available_count()} copies)")
    
    # Livres de programmation disponibles (spec composée)
    programming_available = app.book_repo.find_by_specification(
        BookByCategorySpecification("Programming")
        .and_specification(AvailableBookSpecification())
    )
    print(f"\n[CODE] Programming books available: {len(programming_available)}")
    for book in programming_available:
        print(f"   - {book.title}")
    
    # ========================================================================
    print("\n[CALENDRIER] Step 6: Simulating late return...")
    # ========================================================================
    
    # Recharger Alice
    alice_updated = app.get_member(str(alice.member_id))
    
    if alice_updated and alice_updated.active_loans:
        loan = alice_updated.active_loans[0]
        
        # Simuler retour en retard (5 jours)
        late_return_date = loan.due_date + timedelta(days=5)
        
        print(f"\n[ALARM_CLOCK] Returning book 5 days late...")
        print(f"   Due date: {loan.due_date}")
        print(f"   Return date: {late_return_date}")
        
        result3 = app.return_book(
            member_id=str(alice.member_id),
            loan_id=loan.loan_id,
            return_date=late_return_date
        )
        
        if result3.success:
            if result3.fine_amount:
                print(f"[OK] Book returned with fine")
                print(f"   Fine: {result3.fine_amount}")
            else:
                print(f"[OK] Book returned (no fine)")
    
    # ========================================================================
    print("\n[ARGENT] Step 7: Checking member fines...")
    # ========================================================================
    
    alice_final = app.get_member(str(alice.member_id))
    if alice_final:
        print(f"\n{alice_final.name}:")
        print(f"   Active loans: {alice_final.active_loan_count()}")
        print(f"   Unpaid fines: {alice_final.total_unpaid_fines()}")
        print(f"   Can borrow: {alice_final.can_borrow()}")
    
    # ========================================================================
    print("\n[LISTE] Step 8: Final statistics...")
    # ========================================================================
    
    total_books = len(app.book_repo.find_all())
    total_members = len(app.member_repo.find_all())
    available_books = len(app.find_available_books())
    
    print(f"\nLibrary Statistics:")
    print(f"   Total books: {total_books}")
    print(f"   Available books: {available_books}")
    print(f"   Total members: {total_members}")
    
    print("\n" + "="*80)
    print("[OK] DEMONSTRATION COMPLETE!")
    print("="*80 + "\n")


"""
═══════════════════════════════════════════════════════════════════════════
RÉCAPITULATIF PROJET BIBLIOTHÈQUE
═══════════════════════════════════════════════════════════════════════════
"""

LIBRARY_SUMMARY = """
╔══════════════════════════════════════════════════════════════════════════╗
║          [DOCS] LIBRARY SYSTEM PROJECT - COMPLETE SUMMARY                    ║
╠══════════════════════════════════════════════════════════════════════════╣
║                                                                          ║
║  ARCHITECTURE: Clean Architecture (Uncle Bob)                            ║
║  ─────────────────────────────────────────────────────────────────────   ║
║                                                                          ║
║  ┌─────────────────────────────────────────────────────────┐             ║
║  │     4. FRAMEWORKS & DRIVERS (Infrastructure)            │             ║
║  │        - InMemoryRepositories                           │             ║
║  │        - CLI Interface                                  │             ║
║  └──────────────────────┬──────────────────────────────────┘             ║
║                         │                                                ║
║  ┌──────────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────────────┐             ║
║  │     3. INTERFACE ADAPTERS (Application)                 │             ║
║  │        - Use Cases (Borrow, Return, Reserve)            │             ║
║  │        - DTOs, Ports                                    │             ║
║  └──────────────────────┬──────────────────────────────────┘             ║
║                         │                                                ║
║  ┌──────────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────────────┐             ║
║  │     2. USE CASES (Application Business Rules)           │             ║
║  │        - BorrowBookUseCase                              │             ║
║  │        - ReturnBookUseCase                              │             ║
║  │        - ReserveBookUseCase                             │             ║
║  └──────────────────────┬──────────────────────────────────┘             ║
║                         │                                                ║
║  ┌──────────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────────────┐             ║
║  │     1. ENTITIES (Enterprise Business Rules)             │             ║
║  │        DOMAIN LAYER (0 dependencies)                    │             ║
║  │                                                         │             ║
║  │  VALUE OBJECTS:                                         │             ║
║  │    • ISBN (validation checksum)                         │             ║
║  │    • Money (arithmetic, comparisons)                    │             ║
║  │    • Email, MemberId, DateRange                         │             ║
║  │                                                         │             ║
║  │  ENTITIES:                                              │             ║
║  │    • Book (metadata)                                    │             ║
║  │    • BookCopy (physical instance)                       │             ║
║  │    • Fine (payments)                                    │             ║
║  │                                                         │             ║
║  │  AGGREGATES:                                            │             ║
║  │    • Member (Root - loans, fines, reservations)         │             ║
║  │    • Loan (overdue calc, fine generation)               │             ║
║  │    • Reservation (expiry tracking)                      │             ║
║  │                                                         │             ║
║  │  DOMAIN SERVICES:                                       │             ║
║  │    • LoanPolicy (business rules)                        │             ║
║  │    • FineCalculator (calculations)                      │             ║
║  │                                                         │             ║
║  │  SPECIFICATIONS:                                        │             ║
║  │    • Available, ByCategory, ByAuthor                    │             ║
║  │    • Composable (AND, OR, NOT)                          │             ║
║  └─────────────────────────────────────────────────────────┘             ║
║                                                                          ║
╠══════════════════════════════════════════════════════════════════════════╣
║                                                                          ║
║  DDD TACTICAL PATTERNS APPLIED                                           ║
║  ──────────────────────────────────────────────────────────────────      ║
║                                                                          ║
║  [OK] Value Objects (8 types - immutable, validated)                       ║
║  [OK] Entities (Book, BookCopy, Fine)                                      ║
║  [OK] Aggregates Roots (Member, Loan, Reservation)                         ║
║  [OK] Domain Services (LoanPolicy, FineCalculator)                         ║
║  [OK] Specifications (15+ specs composables)                               ║
║  [OK] Business Rules (explicit, testable)                                  ║
║  [OK] Domain Events (6 events)                                             ║
║  [OK] Ubiquitous Language (vocabulaire métier)                             ║
║  [OK] Invariants (stricts, garantis par aggregates)                        ║
║                                                                          ║
╠══════════════════════════════════════════════════════════════════════════╣
║                                                                          ║
║  BUSINESS RULES IMPLEMENTED                                              ║
║  ──────────────────────────────────────────────────────────────────      ║
║                                                                          ║
║  1. Max loans by type: Standard(3), Premium(10), Student(5)              ║
║  2. Loan duration: Standard(14d), Premium(21d), Student(7d)              ║
║  3. Fine: 0.50€ per day overdue                                          ║
║  4. Max fines threshold: 10€ (suspension)                                ║
║  5. Reservation expiry: 3 days                                           ║
║  6. Damaged book fine: 20€ forfait                                       ║
║  7. Lost book fine: book price + 10€ fees                                ║
║  8. No borrow if: suspended, expired, overdue loans, fines > 10€         ║
║                                                                          ║
╠══════════════════════════════════════════════════════════════════════════╣
║                                                                          ║
║  KEY FEATURES                                                            ║
║  ──────────────────────────────────────────────────────────────────      ║
║                                                                          ║
║  [OK] Clean Architecture (4 layers, dependency rule)                       ║
║  [OK] DDD Tactical (all patterns)                                          ║
║  [OK] Rich Domain Model (behavior + data)                                  ║
║  [OK] Specification Pattern (reusable queries)                             ║
║  [OK] Business Rules explicit                                              ║
║  [OK] Use Cases (clear intentions)                                         ║
║  [OK] Ports & Adapters (DIP)                                               ║
║  [OK] 100% testable domain                                                 ║
║  [OK] TDD (20+ tests)                                                      ║
║                                                                            ║
╚═══════════════════════════════════════════════════════════════════════════╝
"""

def print_summary():
    """Afficher récapitulatif."""
    print(LIBRARY_SUMMARY)


if __name__ == '__main__':
    # Démonstration
    demo_library_system()
    
    # Récapitulatif
    print_summary()

