# ============================================================================
# [LIVRE] TDD (TEST-DRIVEN DEVELOPMENT) - GUIDE ULTRA-DÉTAILLÉ
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR MAÎTRISER LE TDD DE ZÉRO À EXPERT
#
# Ce guide est organisé en 4 parties progressives :
#
# PARTIE 0 : INTRODUCTION ET PHILOSOPHIE (tdd_partie0.md)
# - Chapitre 0 : Qu'est-ce que le TDD ?
# - Chapitre 1 : Pourquoi le TDD ?
# - Chapitre 2 : Le cycle Red-Green-Refactor
# - Chapitre 3 : Mythes et réalités
#
# PARTIE 1 : FONDAMENTAUX (tdd_partie1.md)
# - Chapitre 4 : Setup environnement
# - Chapitre 5 : Premier test TDD
# - Chapitre 6 : Assertions et matchers
# - Chapitre 7 : Test unitaire vs autres types
#
# PARTIE 2 : PRATIQUE AVANCÉE (tdd_partie2.md)
# - Chapitre 8 : Mocks et Stubs
# - Chapitre 9 : Test de code legacy
# - Chapitre 10 : Design patterns TDD
# - Chapitre 11 : TDD avec bases de données
# - Chapitre 12 : TDD avec APIs
#
# PARTIE 3 : MAÎTRISE (tdd_partie3.md)
# - Chapitre 13 : TDD dans différents langages
# - Chapitre 14 : BDD (Behavior-Driven Development)
# - Chapitre 15 : Intégration continue
# - Chapitre 16 : Métriques et code coverage
# - Chapitre 17 : Best practices avancées
#
# [TEMPS] TEMPS DE LECTURE TOTAL : ~20-25 heures
# [DOCS] PRÉREQUIS : Bases programmation (variables, fonctions, classes)
#
# ============================================================================

"""
[OBJECTIF] PHILOSOPHIE DE CE GUIDE

APPROCHE PÉDAGOGIQUE :
- Comprendre le POURQUOI avant le COMMENT
- Exemples concrets et progressifs
- Exercices pratiques à chaque chapitre
- Erreurs courantes expliquées
- Comparaisons code avec/sans TDD

Ce guide vise à TRANSFORMER votre façon de coder !
"""

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

"""
[IDEE] Information importante
[REFLEXION] Question / Réflexion
[OK] Bonne pratique TDD
[X] Anti-pattern / Mauvaise pratique
[ATTENTION] Attention / Piège courant
[CLE] Concept clé à retenir
[COURS] Exercice pratique
[DOCS] Résumé du chapitre
[OBJECTIF] Objectif d'apprentissage
[TEMPS] Temps estimé
[RAPIDE] Point de progression
[THOUGHT_BALLOON] Citation / Sagesse TDD
[ROUGE] Phase RED (test qui échoue)
[VERT] Phase GREEN (test qui passe)
[BLEU] Phase REFACTOR (amélioration)
"""

# ============================================================================
# [GUIDE] CHAPITRE 0 : QU'EST-CE QUE LE TDD ?
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Définir précisément le TDD
[OK] Comprendre le cycle fondamental
[OK] Distinguer TDD des autres approches
[OK] Connaître l'histoire et l'évolution
[OK] Identifier quand utiliser TDD
"""

# ----------------------------------------------------------------------------
# [RECHERCHE] DÉFINITION DU TDD
# ----------------------------------------------------------------------------

"""
TDD = Test-Driven Development
    = Développement Piloté par les Tests

[IDEE] DÉFINITION FORMELLE

Le TDD est une méthodologie de développement logiciel où :

1. On écrit TOUJOURS le test AVANT le code de production
2. On n'écrit que le MINIMUM de code pour faire passer le test
3. On améliore ensuite le code (refactoring) sans changer son comportement

[GUIDE] DÉFINITION KENT BECK (Créateur du TDD)

"TDD is a way of managing fear during programming."
- Kent Beck, "Test-Driven Development: By Example" (2002)


[REFLEXION] COMPARAISON AVEC L'APPROCHE TRADITIONNELLE

APPROCHE TRADITIONNELLE (Test-Last)
────────────────────────────────────
1. Écrire le code
2. Tester manuellement
3. (Peut-être) écrire des tests automatisés
4. Déboguer quand ça casse

[X] Problèmes :
- Tests écrits après coup (souvent bâclés)
- Code difficile à tester
- Beaucoup de bugs en production
- Peur de modifier le code


APPROCHE TDD (Test-First)
──────────────────────────
1. Écrire un test qui échoue
2. Écrire le code minimal pour passer le test
3. Améliorer le code (refactoring)
4. Répéter

[OK] Avantages :
- Code testé à 100%
- Design émergent naturellement
- Confiance pour refactorer
- Documentation vivante
"""

# ----------------------------------------------------------------------------
# [DOC] HISTOIRE ET ORIGINE
# ----------------------------------------------------------------------------

"""
CHRONOLOGIE DU TDD

1999 : NAISSANCE
────────────────
- Kent Beck introduit TDD dans "Extreme Programming Explained"
- Partie intégrante de l'Extreme Programming (XP)
- Révolution dans la communauté Agile

2002 : FORMALISATION
────────────────────
- Publication de "Test-Driven Development: By Example" par Kent Beck
- Le livre de référence sur le sujet
- Démocratisation de la pratique

2003-2010 : ADOPTION MASSIVE
────────────────────────────
- Frameworks de test dans tous les langages
- JUnit (Java), NUnit (.NET), pytest (Python), etc.
- Intégration dans les IDE

2010-PRÉSENT : MATURITÉ
───────────────────────
- Pratique standard dans l'industrie
- Évolution vers BDD (Behavior-Driven Development)
- Outils de plus en plus sophistiqués


[THOUGHT_BALLOON] CITATIONS HISTORIQUES

"I'm not a great programmer; I'm just a good programmer with great habits."
- Kent Beck

"Make it work, make it right, make it fast."
- Kent Beck (Mantra TDD)

"Any fool can write code that a computer can understand. 
Good programmers write code that humans can understand."
- Martin Fowler
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] LE CYCLE FONDAMENTAL : RED-GREEN-REFACTOR
# ----------------------------------------------------------------------------

"""
LE CYCLE TDD EN 3 PHASES

┌─────────────────────────────────────────┐
│    CYCLE RED-GREEN-REFACTOR             │
├─────────────────────────────────────────┤
│                                         │
│   [ROUGE] RED (Rouge)                       │
│   v                                     │
│   Écrire un test qui ÉCHOUE            │
│   (car le code n'existe pas encore)     │
│                                         │
│   [VERT] GREEN (Vert)                      │
│   v                                     │
│   Écrire le code MINIMAL               │
│   pour faire PASSER le test            │
│                                         │
│   [BLEU] REFACTOR (Bleu)                   │
│   v                                     │
│   Améliorer le code                    │
│   (sans changer le comportement)        │
│   v                                     │
│   (sync) Recommencer                         │
│                                         │
└─────────────────────────────────────────┘


DÉTAIL DE CHAQUE PHASE

[ROUGE] PHASE RED (Test qui échoue)
──────────────────────────────

Objectif : Écrire un test qui définit ce que le code DOIT faire

Étapes :
1. Penser à UNE fonctionnalité spécifique
2. Écrire un test qui décrit cette fonctionnalité
3. Lancer le test -> Il DOIT échouer
4. Vérifier que l'échec est pour la BONNE raison

[ATTENTION] Points d'attention :
- Si le test PASSE dès le début -> Problème !
- Le test doit échouer car la fonctionnalité n'existe pas
- Un seul test à la fois


Exemple :
"""

# [X] Code de production n'existe pas encore
def test_addition():
    # On teste quelque chose qui n'existe pas
    calculatrice = Calculatrice()
    resultat = calculatrice.additionner(2, 3)
    assert resultat == 5

# Lancer le test -> ÉCHEC (normal !)
# Erreur : NameError: name 'Calculatrice' is not defined

"""
[VERT] PHASE GREEN (Test qui passe)
───────────────────────────────

Objectif : Écrire le code MINIMAL pour faire passer le test

Étapes :
1. Écrire le code le plus SIMPLE possible
2. Ne PAS penser à l'élégance ou à la performance
3. Juste faire passer le test
4. Lancer le test -> Il DOIT passer

[ATTENTION] Points d'attention :
- MINIMAL = vraiment le minimum !
- Pas de code "au cas où"
- Pas d'optimisation prématurée
- Un seul objectif : test vert


Exemple :
"""

class Calculatrice:
    def additionner(self, a, b):
        # Code MINIMAL pour passer le test
        return 5  # [!] Oui, vraiment !

# Lancer le test -> SUCCÈS ! [OK]

"""
[IDEE] POURQUOI RETOURNER 5 ?

C'est volontairement simpliste pour illustrer :
- On ne code QUE ce qui est testé
- Si le test est trop spécifique, le code aussi
- Prochain test forcera la généralisation

Le test suivant pourrait être :
"""

def test_addition_autres_nombres():
    calculatrice = Calculatrice()
    resultat = calculatrice.additionner(10, 15)
    assert resultat == 25  # Maintenant, "return 5" ne suffit plus !

"""
[BLEU] PHASE REFACTOR (Amélioration)
────────────────────────────────

Objectif : Améliorer le code SANS changer son comportement

Étapes :
1. Observer le code (production + tests)
2. Identifier les duplications, complexités
3. Améliorer progressivement
4. Relancer TOUS les tests après CHAQUE modification
5. Si un test échoue -> annuler le refactoring

[ATTENTION] Points d'attention :
- Les tests ne doivent PAS changer (comportement)
- Modifications incrémentales
- Tests verts à chaque étape
- Pas de nouvelle fonctionnalité ici


Types de refactoring :
- Éliminer duplication
- Renommer variables/fonctions
- Extraire méthodes
- Simplifier conditions
- Améliorer lisibilité


Exemple :
"""

class Calculatrice:
    def additionner(self, a, b):
        # Refactoring : implémenter vraiment l'addition
        return a + b  # Code propre et générique

# Relancer TOUS les tests -> Tous verts ! [OK]

"""
[SYNC] LE CYCLE COMPLET

Durée typique d'un cycle : 2-10 minutes

┌──────────────────────────────────────────────────┐
│                                                  │
│  [ROUGE] RED : 30 secondes - 2 minutes               │
│  (Écrire le test)                                │
│                                                  │
│  [VERT] GREEN : 1-5 minutes                         │
│  (Code minimal)                                  │
│                                                  │
│  [BLEU] REFACTOR : 0-3 minutes                      │
│  (Améliorer)                                     │
│                                                  │
│  (sync) RÉPÉTER pour chaque micro-fonctionnalité    │
│                                                  │
└──────────────────────────────────────────────────┘


[IDEE] RÈGLES D'OR DU CYCLE

1. TOUJOURS commencer par RED
   [X] Si le test passe immédiatement -> Inutile ou mal écrit

2. GREEN le plus vite possible
   [X] Ne pas perdre de temps à "bien" coder

3. REFACTOR seulement si nécessaire
   [OK] Code qui marche > Code parfait

4. Cycle COURT
   [X] Cycle > 10 minutes -> Test trop gros

5. Tests TOUJOURS verts avant commit
   [X] Jamais commit avec tests rouges
"""

# ----------------------------------------------------------------------------
# 🆚 TDD VS AUTRES APPROCHES
# ----------------------------------------------------------------------------

"""
COMPARAISON DÉTAILLÉE


1. TDD vs TEST-LAST (Approche traditionnelle)
──────────────────────────────────────────────

TEST-LAST (Écrire tests après le code)
├─ Avantages :
│  - Plus "naturel" au début
│  - Plus rapide à court terme
│  
└─ Inconvénients :
   - Tests oubliés ou incomplets
   - Code difficile à tester (couplage)
   - Tests qui testent l'implémentation
   - Peu de motivation pour tester

TDD (Écrire tests avant le code)
├─ Avantages :
│  - 100% du code testé
│  - Code découplé (testable)
│  - Tests qui testent le comportement
│  - Confiance et sérénité
│  
└─ Inconvénients :
   - Courbe d'apprentissage
   - Plus lent au début (investissement)


2. TDD vs BDD (Behavior-Driven Development)
────────────────────────────────────────────

TDD
├─ Focus : Comment le code fonctionne (technique)
├─ Niveau : Tests unitaires principalement
├─ Langage : Technique (assert, expect)
└─ Exemple : test_additionner_deux_nombres()

BDD
├─ Focus : Ce que le système fait (métier)
├─ Niveau : Tests d'acceptance principalement
├─ Langage : Naturel (Given-When-Then)
└─ Exemple : "Quand j'additionne 2 et 3, je dois obtenir 5"

[IDEE] BDD = Extension du TDD au niveau fonctionnel


3. TDD vs ATDD (Acceptance Test-Driven Development)
────────────────────────────────────────────────────

TDD
├─ Qui : Développeur
├─ Perspective : Technique
├─ Tests : Unitaires (fonctions, classes)
└─ Cycle : Minutes

ATDD
├─ Qui : Équipe (dev + PO + testeurs)
├─ Perspective : Utilisateur final
├─ Tests : Acceptance (features complètes)
└─ Cycle : Heures/jours

[IDEE] ATDD = TDD au niveau des stories utilisateur


4. TDD vs TFD (Test-First Development)
───────────────────────────────────────

TFD (Test-First)
├─ Écrire test avant code
├─ Faire passer le test
└─ Passer à la suite

TDD (Test-Driven)
├─ Écrire test avant code
├─ Faire passer le test
├─ REFACTOR !  <- La différence clé !
└─ Répéter

[IDEE] TDD = TFD + Refactoring obligatoire


TABLEAU COMPARATIF
──────────────────

┌────────────┬──────────┬──────────┬─────────┬──────────┐
│  Critère   │   TDD    │   BDD    │  ATDD   │Test-Last │
├────────────┼──────────┼──────────┼─────────┼──────────┤
│ Tests avant│   Oui    │   Oui    │   Oui   │   Non    │
│ Refactoring│Obligatoire│ Optionnel│ Rare   │  Rare    │
│ Niveau     │ Unitaire │Acceptance│Acceptance│ Variable │
│ Langage    │Technique │ Naturel  │ Naturel │Technique │
│ Cycle      │ Minutes  │  Heures  │  Jours  │  N/A     │
│ Focus      │  Code    │ Behavior │  User   │  Code    │
└────────────┴──────────┴──────────┴─────────┴──────────┘
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] QUAND UTILISER LE TDD ?
# ----------------------------------------------------------------------------

"""
SITUATIONS IDÉALES POUR LE TDD

[OK] NOUVEAU PROJET
─────────────────
- Partir sur de bonnes bases
- Établir architecture testable
- Créer safety net dès le début

[OK] ALGORITHMES COMPLEXES
────────────────────────
- Logique métier critique
- Calculs financiers
- Algorithmes de tri/recherche
- Parsing de données

Exemple : Calculateur de taxes, moteur de règles métier

[OK] REFACTORING MAJEUR
──────────────────────
- Restructuration code legacy
- Migration de technologie
- Tests = filet de sécurité

[OK] BUG CRITIQUE
───────────────
1. Écrire test qui reproduit le bug
2. Corriger le code
3. Test passe -> Bug fixé
4. Régression impossible !

[OK] API / BIBLIOTHÈQUE
─────────────────────
- Contrat public clair
- Documentation par tests
- Exemples d'utilisation


SITUATIONS OÙ TDD EST MOINS ADAPTÉ

[ATTENTION] PROTOTYPE / SPIKE
────────────────────
- Phase d'exploration
- Apprentissage de technologie
- POC jetable

-> Utiliser TDD une fois direction validée

[ATTENTION] UI / DESIGN VISUEL
──────────────────────
- Mise en page CSS
- Animations
- Design itératif

-> Tests E2E après validation design

[ATTENTION] CODE EXPÉRIMENTAL
────────────────────
- R&D pur
- Algorithmes de recherche
- Optimisation performance

-> TDD une fois algo stabilisé

[ATTENTION] DEADLINES TRÈS SERRÉES
──────────────────────────
- Pression temporelle extrême
- Contexte exceptionnel

[ATTENTION] Mais : Dette technique à rembourser !


CRITÈRES DE DÉCISION

Utilisez TDD quand :
[x] Le code doit être fiable
[x] Le code sera maintenu longtemps
[x] Le code est complexe
[x] L'équipe connaît TDD
[x] Le projet a du temps

Évitez TDD quand :
[ ] Prototype jetable
[ ] Équipe totalement novice
[ ] Contexte extrême (rare !)


[IDEE] RÈGLE GÉNÉRALE

"If it's important enough to write,
 it's important enough to test first."

En cas de doute -> TDD !
Le coût initial est vite rentabilisé.
"""

# ----------------------------------------------------------------------------
# [GRAPHIQUE] STATISTIQUES ET ÉTUDES
# ----------------------------------------------------------------------------

"""
DONNÉES SCIENTIFIQUES SUR LE TDD


ÉTUDE IBM (2003)
────────────────
- Projet : Driver USB (Windows)
- Résultat : 40% moins de bugs en production
- Temps : +15% de développement
- ROI : Largement positif

ÉTUDE MICROSOFT (2008)
──────────────────────
- Projets : Windows, Visual Studio
- Résultat : 60-90% réduction défauts
- Temps : +15-35% développement
- Conclusion : "TDD significantly reduces defect density"

ÉTUDE NORTH CAROLINA STATE UNIVERSITY (2010)
─────────────────────────────────────────────
- 24 développeurs professionnels
- Résultat : 
  * TDD : 18% plus de temps
  * TDD : 50% moins de bugs
  * Tests TDD : Meilleure couverture

META-ANALYSE (2014)
───────────────────
- 27 études analysées
- Consensus :
  * +15-20% temps développement
  * -40-90% bugs production
  * Meilleure qualité code
  * Meilleure maintenabilité


CHIFFRES CLÉS
─────────────

[GRAPHIQUE] Bugs en production
   TDD : -40% à -90%

[GRAPHIQUE] Temps de développement
   TDD : +15% à +35%

[GRAPHIQUE] Temps de debugging
   TDD : -50% à -70%

[GRAPHIQUE] Confiance pour refactorer
   TDD : +300%

[GRAPHIQUE] Documentation vivante
   TDD : 100% (tests = doc)


[IDEE] INTERPRÉTATION

Temps de dev +20% MAIS :
- Moins de bugs -> Moins de hotfix
- Moins de debugging
- Refactoring serein
- Onboarding facilité

-> ROI positif à moyen/long terme !
"""

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Définition précise du TDD
   - Écrire test AVANT code
   - Code minimal pour passer test
   - Refactoring obligatoire

[OK] Histoire et origine
   - Kent Beck (1999-2002)
   - Partie de Extreme Programming
   - Adoption massive depuis

[OK] Cycle Red-Green-Refactor
   - [ROUGE] Test qui échoue
   - [VERT] Code minimal
   - [BLEU] Amélioration
   - Cycle court (2-10 min)

[OK] TDD vs autres approches
   - Test-Last, BDD, ATDD, TFD
   - Avantages et contextes

[OK] Quand utiliser TDD
   - Nouveau projet
   - Algorithmes complexes
   - Refactoring
   - Bug fixing

[OK] Données scientifiques
   - -40 à -90% bugs
   - +15 à +35% temps dev
   - ROI positif moyen/long terme


[CLE] POINTS CLÉS À RETENIR

1. TDD = Discipline de développement
   Pas juste "écrire des tests"

2. Test AVANT code = Fondamental
   Change complètement l'approche

3. Cycle court et itératif
   Petits pas, feedback constant

4. Refactoring = Partie intégrante
   Pas optionnel !

5. Investissement rentable
   Coût initial compensé rapidement


[THOUGHT_BALLOON] CITATION À MÉDITER

"TDD is not about testing.
 TDD is about design."
- Uncle Bob Martin


[RAPIDE] PRÊT POUR LA SUITE ?

Maintenant que vous comprenez QUOI et POURQUOI,
passons au COMMENT dans le Chapitre 1 !
"""

# ============================================================================
# [GUIDE] CHAPITRE 1 : POURQUOI LE TDD ?
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous comprendrez :
[OK] Les bénéfices concrets du TDD
[OK] Les problèmes que TDD résout
[OK] L'impact sur la qualité du code
[OK] L'impact sur l'équipe
[OK] Le ROI du TDD
"""

# ----------------------------------------------------------------------------
# [GEM_STONE] LES BÉNÉFICES DU TDD
# ----------------------------------------------------------------------------

"""
7 BÉNÉFICES MAJEURS DU TDD


1. QUALITÉ DE CODE SUPÉRIEURE
──────────────────────────────

Sans TDD :
"""

# [X] Code couplé, difficile à tester
class UserService:
    def __init__(self):
        self.db = Database()  # Dépendance dure
        self.email = EmailService()  # Couplage
        self.logger = Logger()
    
    def create_user(self, name, email):
        # Logique mélangée
        user = self.db.insert_user(name, email)
        self.email.send_welcome(email)
        self.logger.log(f"User {name} created")
        return user

# Comment tester ça ?
# - Besoin d'une vraie DB
# - Envoie vraiment des emails
# - Couplé à tout

"""
Avec TDD :
"""

# [OK] Code découplé, testable
class UserService:
    def __init__(self, db, email_service, logger):
        # Injection de dépendances
        self.db = db
        self.email = email_service
        self.logger = logger
    
    def create_user(self, name, email):
        user = self.db.insert_user(name, email)
        self.email.send_welcome(email)
        self.logger.log(f"User {name} created")
        return user

# Test facile avec mocks !

"""
[IDEE] POURQUOI TDD AMÉLIORE LE CODE ?

TDD force à écrire du code :
[OK] Découplé (injection dépendances)
[OK] Modulaire (petites fonctions)
[OK] Simple (YAGNI appliqué)
[OK] Cohérent (interface claire)

"If it's hard to test, it's hard to use"
-> TDD guide vers bon design


2. MOINS DE BUGS
────────────────

STATISTIQUES RÉELLES

Projet sans TDD :
├─ 10-50 bugs / 1000 lignes code
├─ 30-50% temps en debugging
└─ Bugs découverts en production

Projet avec TDD :
├─ 0.5-5 bugs / 1000 lignes code
├─ 5-10% temps en debugging
└─ Bugs découverts en dev


TYPES DE BUGS ÉVITÉS

[OK] Bugs de régression
   Test échoue immédiatement

[OK] Bugs d'edge cases
   Tests couvrent cas limites

[OK] Bugs d'intégration
   Tests détectent incompatibilités

[OK] Bugs de logique métier
   Tests = spécifications exécutables


EXEMPLE CONCRET
"""

# Sans TDD - Bug caché
def calculer_remise(prix, pourcentage):
    return prix - (prix * pourcentage)

# Utilisé avec : calculer_remise(100, 20)
# Retourne : -1900 au lieu de 80 ! [!]
# Bug : pourcentage devrait être 0.20 pas 20

"""
Avec TDD - Bug détecté immédiatement
"""

def test_calculer_remise():
    # Test écrit AVANT le code
    resultat = calculer_remise(100, 20)
    assert resultat == 80  # Échec immédiat !

# Force à clarifier : pourcentage en % ou décimal ?

"""
3. REFACTORING SEREIN
─────────────────────

Sans TDD :
"""

# [X] Peur de casser quelque chose
def calculer_total(panier):
    # Code complexe et fragile
    total = 0
    for item in panier.items:
        if item.promo:
            if item.promo_type == "percentage":
                total += item.prix * (1 - item.promo_value/100)
            elif item.promo_type == "fixed":
                total += item.prix - item.promo_value
        else:
            total += item.prix
    
    if panier.has_voucher:
        if panier.voucher_type == "percentage":
            total = total * (1 - panier.voucher_value/100)
        # ... encore plus de if
    
    return total

# Personne n'ose toucher à ce code ! [!]

"""
Avec TDD :
"""

# [OK] Tests = filet de sécurité
def test_total_sans_promo():
    panier = Panier([Item(prix=10), Item(prix=20)])
    assert calculer_total(panier) == 30

def test_total_avec_promo_pourcentage():
    item = Item(prix=100, promo=Promo("percentage", 20))
    panier = Panier([item])
    assert calculer_total(panier) == 80

def test_total_avec_voucher():
    # ... plus de tests

# Maintenant on peut refactorer !
def calculer_total(panier):
    total = sum(item.prix_avec_promo() for item in panier.items)
    return panier.appliquer_voucher(total)

# Tests passent -> Refactoring OK ! [OK]

"""
[IDEE] CONFIANCE = AGILITÉ

Tests = Safety net
-> Refactoring sans peur
-> Amélioration continue
-> Dette technique évitée


4. DOCUMENTATION VIVANTE
────────────────────────

Documentation traditionnelle :
"""

# [X] README.md (souvent obsolète)
"""
# API Calculatrice

## Méthode additionner()
Additionne deux nombres.

Paramètres:
- a: premier nombre
- b: deuxième nombre

Retourne: la somme

Exemple: additionner(2, 3) retourne 5
"""

# Problèmes :
# - Peut-être obsolète
# - Pas exécutable
# - Pas de garantie

"""
Tests TDD = Documentation exécutable :
"""

# [OK] Tests comme documentation
def test_additionner_nombres_positifs():
    """Additionner deux nombres positifs"""
    calc = Calculatrice()
    assert calc.additionner(2, 3) == 5

def test_additionner_nombres_negatifs():
    """Fonctionne aussi avec nombres négatifs"""
    calc = Calculatrice()
    assert calc.additionner(-2, -3) == -5

def test_additionner_zero():
    """Addition avec zéro"""
    calc = Calculatrice()
    assert calc.additionner(5, 0) == 5

def test_additionner_grands_nombres():
    """Gère les grands nombres"""
    calc = Calculatrice()
    assert calc.additionner(1_000_000, 2_000_000) == 3_000_000

# Documentation :
# [OK] Toujours à jour (sinon test échoue)
# [OK] Exécutable (preuve que ça marche)
# [OK] Exemples concrets d'utilisation

"""
[IDEE] AVANTAGES DOCUMENTATION PAR TESTS

[OK] Jamais obsolète (tests lancés régulièrement)
[OK] Exemples réels (comment utiliser l'API)
[OK] Cas limites documentés (edge cases)
[OK] Preuve de fonctionnement (exécutable)


5. DESIGN ÉMERGENT
──────────────────

TDD favorise l'émergence d'un bon design :
"""

# Cycle TDD guide naturellement vers :

# SOLID Principles
# ├─ S : Single Responsibility
# │    (Tests forcent à séparer responsabilités)
# │
# ├─ O : Open/Closed
# │    (Refactoring améliore extensibilité)
# │
# ├─ L : Liskov Substitution
# │    (Mocks nécessitent interfaces cohérentes)
# │
# ├─ I : Interface Segregation
# │    (Tests montrent interfaces trop larges)
# │
# └─ D : Dependency Inversion
#      (Tests nécessitent injection dépendances)

"""
Exemple d'évolution du design avec TDD :
"""

# [ROUGE] RED : Test initial
def test_envoyer_notification_email():
    service = NotificationService()
    service.envoyer("user@example.com", "Hello")
    # Comment vérifier sans envoyer vraiment ?

# [VERT] GREEN : Première implémentation
class NotificationService:
    def envoyer(self, destinataire, message):
        # Envoi réel (problème pour test)
        smtp.send(destinataire, message)

# [BLEU] REFACTOR : Amélioration design
# Tests forcent injection de dépendance
class NotificationService:
    def __init__(self, email_sender):
        self.email_sender = email_sender  # Interface !
    
    def envoyer(self, destinataire, message):
        self.email_sender.send(destinataire, message)

# Maintenant testable avec un mock !

"""
6. FEEDBACK RAPIDE
──────────────────

Sans TDD :
"""

# [X] Cycle de feedback lent

Écrire code -> Compiler -> Déployer -> Test manuel
  v           v          v          v
 5 min      2 min      10 min     5 min
                                    v
                              Bug trouvé !
                                    v
                          Recommencer... [TIRED_FACE]

# Total : ~25 minutes par essai

"""
Avec TDD :
"""

# [OK] Cycle de feedback ultra-rapide

Écrire test -> Lancer test -> Correction
    v              v             v
  30 sec        5 sec         1 min
                                v
                           Test vert ! [OK]

# Total : ~2 minutes par cycle

"""
[IDEE] IMPACT DU FEEDBACK RAPIDE

[OK] Itérations plus nombreuses
[OK] Apprentissage accéléré
[OK] Moins de frustration
[OK] Flow state maintenu

"The faster you can iterate,
 the faster you can learn."


7. SPÉCIFICATIONS EXÉCUTABLES
──────────────────────────────

Tests TDD = Contrat vivant
"""

# Spécification métier :
"""
Une commande est validée si :
- Total > 0
- Au moins un article
- Adresse de livraison présente
- Moyen de paiement valide
"""

# Tests = Spécification exécutable :

def test_commande_valide():
    """Une commande valide passe la validation"""
    commande = Commande(
        articles=[Article(nom="Livre", prix=10)],
        adresse=Adresse("123 rue", "Paris"),
        paiement=CarteBancaire(valide=True)
    )
    assert commande.est_valide()

def test_commande_sans_article():
    """Une commande sans article est invalide"""
    commande = Commande(
        articles=[],
        adresse=Adresse("123 rue", "Paris"),
        paiement=CarteBancaire(valide=True)
    )
    assert not commande.est_valide()

def test_commande_sans_adresse():
    """Une commande sans adresse est invalide"""
    commande = Commande(
        articles=[Article(nom="Livre", prix=10)],
        adresse=None,
        paiement=CarteBancaire(valide=True)
    )
    assert not commande.est_valide()

# Tests = Spécifications précises et vérifiables !

"""
[IDEE] AVANTAGES SPÉCIFICATIONS EXÉCUTABLES

[OK] Ambiguïté éliminée (code précis)
[OK] Validation automatique (conformité)
[OK] Exemples concrets (compréhension)
[OK] Régression impossible (protection)
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] IMPACT SUR L'ÉQUIPE
# ----------------------------------------------------------------------------

"""
BÉNÉFICES COLLECTIFS DU TDD


1. COLLABORATION AMÉLIORÉE
──────────────────────────

Tests = Langage commun
"""

# Développeur -> QA :
def test_remise_appliquee_correctement():
    """Le QA peut lire et comprendre ce test"""
    panier = Panier()
    panier.ajouter(Produit(prix=100))
    panier.appliquer_remise(20)  # 20%
    
    assert panier.total() == 80

# QA valide : "OK, c'est bien le comportement attendu"

"""
2. ONBOARDING FACILITÉ
──────────────────────

Nouveau développeur :
"""

# Lit les tests pour comprendre le code
def test_workflow_creation_utilisateur():
    """
    Tests documentent le processus complet
    Parfait pour comprendre le système !
    """
    # 1. Créer utilisateur
    user = UserService.create(email="test@example.com")
    
    # 2. Vérifier email envoyé
    assert email_sent_to(user.email)
    
    # 3. Activer compte
    user.activate(token_from_email())
    
    # 4. Vérifier activation
    assert user.is_active

# Nouveau dev comprend le workflow en lisant les tests

"""
3. CONFIANCE COLLECTIVE
───────────────────────

Sans tests :
"""
# [X] "Je ne touche pas à ce code"
# [X] "C'est Jean qui l'a écrit, demande-lui"
# [X] "On ne sait pas si ça marche ailleurs"
# [X] Bus factor élevé

"""
Avec TDD :
"""
# [OK] "Les tests passent, je peux modifier"
# [OK] "Le comportement est documenté"
# [OK] "Si je casse quelque chose, je le saurai"
# [OK] Bus factor réduit

"""
4. REVUES DE CODE FACILITÉES
─────────────────────────────

Pull Request avec TDD :
"""

# Reviewer peut :
# 1. Lire tests pour comprendre intention
# 2. Lancer tests pour vérifier
# 3. Voir coverage pour gaps
# 4. Suggérer tests manquants

# Commentaire typique :
"""
@dev: Code looks good! 
Tests cover the happy path.
Could you add a test for the case where user is null?
"""

"""
5. VÉLOCITÉ STABLE
──────────────────

Sans TDD :
         Rapidité
            ^
            |  ╱╲
            | ╱  ╲___
            |╱       ╲___
            |____________╲
            Début    Temps ->
            
# Rapide au début, puis :
# - Bugs s'accumulent
# - Peur de toucher code
# - Dette technique
# - Productivité baisse

Avec TDD :
         Rapidité
            ^
            | ___
            |╱   ───────
            |
            |____________
            Début    Temps ->

# Plus lent au début, puis :
# - Vélocité stable
# - Pas de régression
# - Refactoring facile
# - Productivité maintenue
"""

# ----------------------------------------------------------------------------
# [ARGENT] ROI (Return On Investment)
# ----------------------------------------------------------------------------

"""
CALCUL DU RETOUR SUR INVESTISSEMENT


COÛTS DU TDD
────────────

Initial :
├─ Formation équipe : 2-4 semaines
├─ Courbe apprentissage : 2-3 mois
└─ +15-35% temps développement features

Récurrent :
└─ Maintenance tests (< 5% temps)


GAINS DU TDD
────────────

Immédiats (Semaines 1-4) :
├─ Moins de bugs en dev
└─ Moins de debugging

Court terme (Mois 1-3) :
├─ -50% bugs production
├─ -70% temps debugging
└─ Refactoring serein

Moyen terme (Mois 3-12) :
├─ Vélocité stable
├─ Onboarding plus rapide
├─ Documentation vivante
└─ Dette technique évitée

Long terme (An 1+) :
├─ Maintenance simplifiée
├─ Évolutions facilitées
├─ Coût de possession réduit
└─ Satisfaction équipe


EXEMPLE CHIFFRÉ (Équipe de 5 devs)
───────────────────────────────────

Sans TDD :
├─ Bugs production : 20/mois
├─ Temps fix bugs : 40h/mois
├─ Coût bugs : 40h × 5 devs × 50€/h = 10 000€/mois
└─ Total an 1 : 120 000€

Avec TDD :
├─ Formation : 10 000€ (one-time)
├─ Temps dev +20% : +1 dev = 50 000€/an
├─ Bugs production : 2/mois
├─ Temps fix bugs : 4h/mois
├─ Coût bugs : 4h × 5 devs × 50€/h = 1 000€/mois
├─ Total an 1 : 72 000€
└─ Économie : 48 000€/an !

ROI : Rentable dès 6-9 mois


GAINS NON QUANTIFIABLES
────────────────────────

[OK] Sérénité développeurs
[OK] Confiance déploiements
[OK] Satisfaction client (moins bugs)
[OK] Réputation code quality
[OK] Rétention talents (devs aiment TDD)
"""

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] 7 bénéfices majeurs du TDD
   1. Qualité code supérieure
   2. Moins de bugs (-40 à -90%)
   3. Refactoring serein
   4. Documentation vivante
   5. Design émergent
   6. Feedback rapide
   7. Spécifications exécutables

[OK] Impact sur l'équipe
   - Collaboration améliorée
   - Onboarding facilité
   - Confiance collective
   - Reviews simplifiées
   - Vélocité stable

[OK] ROI du TDD
   - Coût initial : Formation
   - Gains : -50% bugs, -70% debug
   - Rentabilité : 6-9 mois
   - Gains long terme : Énormes


[CLE] POINTS CLÉS À RETENIR

1. TDD = Investissement rentable
   Coût court terme < Gains moyen/long terme

2. Qualité > Vitesse
   "Slow is smooth, smooth is fast"

3. Tests = Actif, pas coût
   Documentation + Filet sécurité

4. TDD change la culture
   Impact au-delà du code

5. Bénéfices cumulatifs
   Plus longtemps TDD, plus de gains


[THOUGHT_BALLOON] CITATIONS À MÉDITER

"The only way to go fast is to go well."
- Uncle Bob Martin

"Quality is free, but only to those
 who are willing to pay heavily for it."
- DeMarco & Lister

"I'm not a great programmer,
 I'm just a good programmer with great habits."
- Kent Beck


[RAPIDE] PROCHAINE ÉTAPE

Maintenant que vous savez POURQUOI,
découvrons le cycle en détail au Chapitre 2 !
"""

# ============================================================================
# FIN DE PARTIE 0 - SUITE DANS tdd_partie0_suite.md
# ============================================================================

# ============================================================================
# [LIVRE] TDD - PARTIE 0 (SUITE) : CYCLE RED-GREEN-REFACTOR DÉTAILLÉ
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 2 : LE CYCLE RED-GREEN-REFACTOR EN PROFONDEUR
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Maîtriser chaque phase du cycle
[OK] Reconnaître les erreurs courantes
[OK] Appliquer les bonnes pratiques
[OK] Gérer les situations complexes
[OK] Maintenir le rythme TDD
"""

# ----------------------------------------------------------------------------
# [ROUGE] PHASE RED : ÉCRIRE UN TEST QUI ÉCHOUE
# ----------------------------------------------------------------------------

"""
LA PHASE RED EN DÉTAIL

Durée : 30 secondes - 2 minutes
Objectif : Définir CLAIREMENT ce que le code doit faire


ÉTAPES DÉTAILLÉES

1. CHOISIR UNE MICRO-FONCTIONNALITÉ
───────────────────────────────────
"""

# [X] TROP GROS (plusieurs fonctionnalités)
def test_systeme_utilisateur():
    # Créer utilisateur
    # Valider email
    # Envoyer notification
    # Logger activité
    # Mettre à jour statistiques
    pass

# [OK] BON (une seule chose)
def test_creer_utilisateur_avec_email_valide():
    """Teste UNIQUEMENT la création"""
    service = UserService()
    user = service.create_user("alice@example.com")
    assert user.email == "alice@example.com"

"""
[IDEE] RÈGLE D'OR : ONE TEST, ONE ASSERTION (idéalement)

Un test = Un concept métier = Un comportement


2. ÉCRIRE LE TEST COMME SI LE CODE EXISTAIT
────────────────────────────────────────────
"""

# Penser à l'API idéale, pas à l'implémentation

# [X] MAUVAIS (pense implémentation)
def test_addition():
    calc = Calculatrice()
    calc.nombre1 = 5
    calc.nombre2 = 3
    calc.calculer()
    assert calc.resultat == 8

# [OK] BON (API claire et simple)
def test_addition():
    calc = Calculatrice()
    resultat = calc.additionner(5, 3)
    assert resultat == 8

"""
[IDEE] TDD = Test-First DESIGN

Le test définit l'API publique
-> Force à penser utilisabilité AVANT implémentation


3. LANCER LE TEST -> IL DOIT ÉCHOUER
────────────────────────────────────
"""

# Le test DOIT échouer pour la BONNE raison

# Exemple :
def test_division_par_zero():
    calc = Calculatrice()
    with pytest.raises(ValueError):
        calc.diviser(10, 0)

# Première exécution :
# [X] NameError: name 'Calculatrice' is not defined
# [OK] C'est la bonne raison (classe n'existe pas)

# Après création classe vide :
# [X] AttributeError: no method 'diviser'
# [OK] Encore la bonne raison (méthode n'existe pas)

# Après création méthode qui retourne 42 :
# [X] Test passes (pas de ValueError levée)
# [X] MAUVAISE raison ! La méthode ne fait pas ce qu'elle doit

"""
[ATTENTION] PIÈGES À ÉVITER EN PHASE RED


PIÈGE 1 : Test qui passe immédiatement
───────────────────────────────────────
"""

def test_addition():
    # Oups, le code existe déjà !
    resultat = 2 + 3  # Python built-in
    assert resultat == 5  # [OK] Passe immédiatement

# [IDEE] Si test passe dès le début -> Inutile ou mal écrit

"""
PIÈGE 2 : Test trop large
─────────────────────────
"""

def test_tout_le_systeme():
    # Test trop gros
    user = create_user()
    post = create_post(user)
    comment = add_comment(post)
    like = add_like(comment)
    # ... 50 lignes plus tard
    assert something

# [IDEE] Si test > 10 lignes -> Probablement trop gros

"""
PIÈGE 3 : Test couplé à l'implémentation
─────────────────────────────────────────
"""

def test_mauvais():
    calc = Calculatrice()
    # Test interne implementation
    assert calc._internal_buffer == []  # [X]
    assert calc._cache is not None  # [X]

# [OK] Tester le COMPORTEMENT, pas l'implémentation

"""
PIÈGE 4 : Assertion peu claire
───────────────────────────────
"""

def test_unclear():
    result = process_data(input_data)
    assert result  # [X] Peu clair : True ? Not empty ? Not None ?

# [OK] Assertion explicite
def test_clear():
    result = process_data(input_data)
    assert result.success is True
    assert len(result.errors) == 0

"""
BONNES PRATIQUES PHASE RED
───────────────────────────

[OK] Nom de test descriptif
   test_additionner_deux_nombres_positifs()
   test_diviser_par_zero_leve_erreur()

[OK] Pattern AAA (Arrange-Act-Assert)
   # Arrange : Préparer
   calc = Calculatrice()
   
   # Act : Agir
   resultat = calc.additionner(2, 3)
   
   # Assert : Vérifier
   assert resultat == 5

[OK] Test isolé et indépendant
   # Pas de dépendance entre tests
   # Chaque test peut s'exécuter seul

[OK] Test rapide
   # < 100ms idéalement
   # Pas de I/O si possible
"""

# ----------------------------------------------------------------------------
# [VERT] PHASE GREEN : FAIRE PASSER LE TEST
# ----------------------------------------------------------------------------

"""
LA PHASE GREEN EN DÉTAIL

Durée : 1-5 minutes
Objectif : Code MINIMAL pour test vert


PRINCIPE FONDAMENTAL : FAKE IT TILL YOU MAKE IT

Étape 1 : Faire passer avec une constante (fake)
Étape 2 : Généraliser progressivement
Étape 3 : Laisser les tests guider


EXEMPLE : FIBONACCI

[ROUGE] RED : Premier test
"""

def test_fibonacci_0():
    assert fibonacci(0) == 0

# [VERT] GREEN : Solution la plus simple
def fibonacci(n):
    return 0  # [OK] Test passe !

"""
[ROUGE] RED : Deuxième test
"""

def test_fibonacci_1():
    assert fibonacci(1) == 1

# [VERT] GREEN : Encore fake, mais plus malin
def fibonacci(n):
    if n == 0:
        return 0
    return 1  # [OK] Les deux tests passent !

"""
[ROUGE] RED : Troisième test
"""

def test_fibonacci_2():
    assert fibonacci(2) == 1

# [VERT] GREEN : Toujours fake...
def fibonacci(n):
    if n == 0:
        return 0
    if n == 2:
        return 1
    return 1  # [OK] Trois tests passent !

"""
[ROUGE] RED : Quatrième test (force la généralisation)
"""

def test_fibonacci_3():
    assert fibonacci(3) == 2

# [VERT] GREEN : Maintenant OBLIGÉ de généraliser
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)  # [OK] Vrai algo !

"""
[IDEE] POURQUOI CETTE APPROCHE ?

1. Chaque étape est SIMPLE
2. On ne sur-ingénierie pas
3. Les tests FORCENT la généralisation au bon moment
4. On code seulement ce qui est testé


STRATÉGIES PHASE GREEN

Stratégie 1 : Fake It (Constante)
──────────────────────────────────
"""

# Test
def test_get_greeting():
    assert get_greeting("Alice") == "Hello, Alice!"

# Implémentation la plus simple
def get_greeting(name):
    return "Hello, Alice!"  # [!] Hard-coded !

# Prochain test forcera généralisation
def test_get_greeting_bob():
    assert get_greeting("Bob") == "Hello, Bob!"

# Maintenant obligé de généraliser
def get_greeting(name):
    return f"Hello, {name}!"

"""
Stratégie 2 : Obvious Implementation
─────────────────────────────────────
"""

# Si l'implémentation est évidente, pas besoin de fake

def test_multiply():
    assert multiply(3, 4) == 12

# Implémentation directe (évidente)
def multiply(a, b):
    return a * b  # Pas besoin de fake ici

"""
Stratégie 3 : Triangulation
────────────────────────────
"""

# Ajouter plusieurs tests pour forcer la généralisation

def test_calculer_remise_10_pourcent():
    assert calculer_remise(100, 10) == 90

def test_calculer_remise_20_pourcent():
    assert calculer_remise(100, 20) == 80

def test_calculer_remise_50_pourcent():
    assert calculer_remise(200, 50) == 100

# Trois tests -> Force l'implémentation générique
def calculer_remise(prix, pourcentage):
    return prix - (prix * pourcentage / 100)

"""
[ATTENTION] ERREURS COURANTES PHASE GREEN


ERREUR 1 : Sur-ingénierie
──────────────────────────
"""

# Test simple
def test_addition():
    calc = Calculatrice()
    assert calc.additionner(2, 3) == 5

# [X] MAUVAIS : Code trop complexe
class Calculatrice:
    def __init__(self):
        self.history = []
        self.cache = {}
        self.observers = []
    
    def additionner(self, a, b):
        # Vérifie cache
        key = f"{a}+{b}"
        if key in self.cache:
            return self.cache[key]
        
        # Calcule
        result = a + b
        
        # Sauvegarde historique
        self.history.append((a, b, result))
        
        # Notifie observers
        for obs in self.observers:
            obs.notify(result)
        
        # Cache
        self.cache[key] = result
        
        return result

# [OK] BON : Code minimal
class Calculatrice:
    def additionner(self, a, b):
        return a + b

# YAGNI : You Ain't Gonna Need It !

"""
ERREUR 2 : Optimisation prématurée
───────────────────────────────────
"""

# Test
def test_find_user():
    users = [User("Alice"), User("Bob")]
    assert find_user(users, "Bob").name == "Bob"

# [X] MAUVAIS : Optimisation prématurée
def find_user(users, name):
    # Utilise hash table pour O(1) lookup
    user_dict = {u.name: u for u in users}
    return user_dict.get(name)

# [OK] BON : Solution simple (test passe)
def find_user(users, name):
    for user in users:
        if user.name == name:
            return user

# Optimiser plus tard si nécessaire (après profiling)

"""
ERREUR 3 : Tester l'implémentation au lieu du comportement
───────────────────────────────────────────────────────────
"""

# [X] MAUVAIS : Test couplé à l'implémentation
def test_mauvais():
    calc = Calculatrice()
    calc.additionner(2, 3)
    # Test interne
    assert len(calc.operations) == 1  # [X]
    assert calc.operations[0] == "add"  # [X]

# [OK] BON : Test du comportement observable
def test_bon():
    calc = Calculatrice()
    result = calc.additionner(2, 3)
    assert result == 5  # [OK] Comportement public

"""
BONNES PRATIQUES PHASE GREEN
─────────────────────────────

[OK] Code le plus SIMPLE possible
   if -> else -> loop -> recursion (ordre de simplicité)

[OK] Pas de code "au cas où"
   Seulement ce qui est testé

[OK] Pas d'abstraction prématurée
   "Duplication is better than wrong abstraction"

[OK] Baby steps
   Petits incréments, feedback constant

[OK] Test vert RAPIDEMENT
   Si bloqué > 5 min -> Test trop gros
"""

# ----------------------------------------------------------------------------
# [BLEU] PHASE REFACTOR : AMÉLIORER LE CODE
# ----------------------------------------------------------------------------

"""
LA PHASE REFACTOR EN DÉTAIL

Durée : 0-3 minutes (peut être 0 si code déjà propre)
Objectif : Améliorer SANS changer comportement


PRINCIPE FONDAMENTAL : RED-GREEN-REFACTOR, PAS RED-REFACTOR

[ATTENTION] Toujours avoir tests VERTS avant de refactorer !


QUAND REFACTORER ?
──────────────────

Signaux qu'il faut refactorer :

1. DUPLICATION
"""

# [ROUGE]-[VERT] Code après plusieurs cycles
class Calculatrice:
    def additionner(self, a, b):
        print(f"Operation: addition")
        result = a + b
        print(f"Result: {result}")
        return result
    
    def soustraire(self, a, b):
        print(f"Operation: soustraction")
        result = a - b
        print(f"Result: {result}")
        return result
    
    def multiplier(self, a, b):
        print(f"Operation: multiplication")
        result = a * b
        print(f"Result: {result}")
        return result

# [BLEU] REFACTOR : Éliminer duplication
class Calculatrice:
    def _log_operation(self, operation, result):
        print(f"Operation: {operation}")
        print(f"Result: {result}")
    
    def additionner(self, a, b):
        result = a + b
        self._log_operation("addition", result)
        return result
    
    def soustraire(self, a, b):
        result = a - b
        self._log_operation("soustraction", result)
        return result
    
    def multiplier(self, a, b):
        result = a * b
        self._log_operation("multiplication", result)
        return result

# Relancer tests -> Tous verts ! [OK]

"""
2. COMPLEXITÉ
"""

# Code devenu complexe
def valider_commande(commande):
    if commande.articles and len(commande.articles) > 0:
        if commande.adresse is not None:
            if commande.adresse.rue and commande.adresse.ville:
                if commande.paiement:
                    if commande.paiement.valide:
                        if commande.total > 0:
                            return True
    return False

# [BLEU] REFACTOR : Simplifier
def valider_commande(commande):
    return (
        commande.a_des_articles() and
        commande.a_adresse_complete() and
        commande.a_paiement_valide() and
        commande.total > 0
    )

"""
3. NOMS PEU CLAIRS
"""

# Noms cryptiques
def calc(x, y, op):
    if op == 1:
        return x + y
    elif op == 2:
        return x - y
    return None

# [BLEU] REFACTOR : Noms explicites
def calculer(premier_nombre, second_nombre, operation):
    if operation == Operation.ADDITION:
        return premier_nombre + second_nombre
    elif operation == Operation.SOUSTRACTION:
        return premier_nombre - second_nombre
    return None

"""
TYPES DE REFACTORING COURANTS
──────────────────────────────

1. Extract Method (Extraire méthode)
────────────────────────────────────
"""

# Avant
def process_order(order):
    # Valider commande
    if not order.items:
        raise ValueError("No items")
    if not order.address:
        raise ValueError("No address")
    
    # Calculer total
    total = 0
    for item in order.items:
        if item.promo:
            total += item.price * 0.8
        else:
            total += item.price
    
    # Envoyer email
    msg = f"Order total: {total}"
    send_email(order.email, msg)

# Après refactoring
def process_order(order):
    validate_order(order)
    total = calculate_total(order)
    send_confirmation_email(order, total)

def validate_order(order):
    if not order.items:
        raise ValueError("No items")
    if not order.address:
        raise ValueError("No address")

def calculate_total(order):
    return sum(
        item.price * (0.8 if item.promo else 1.0)
        for item in order.items
    )

def send_confirmation_email(order, total):
    msg = f"Order total: {total}"
    send_email(order.email, msg)

"""
2. Rename (Renommer)
────────────────────
"""

# Avant
def calc_t(o):
    t = 0
    for i in o.i:
        t += i.p
    return t

# Après
def calculer_total_commande(commande):
    total = 0
    for article in commande.articles:
        total += article.prix
    return total

"""
3. Replace Magic Number (Remplacer nombre magique)
───────────────────────────────────────────────────
"""

# Avant
def is_adult(age):
    return age >= 18

def can_drive(age):
    return age >= 18

def can_vote(age):
    return age >= 18

# Après
LEGAL_AGE = 18

def is_adult(age):
    return age >= LEGAL_AGE

def can_drive(age):
    return age >= LEGAL_AGE

def can_vote(age):
    return age >= LEGAL_AGE

"""
4. Introduce Parameter Object
──────────────────────────────
"""

# Avant : Trop de paramètres
def creer_facture(nom, prenom, rue, ville, code_postal, 
                  article1, article2, article3, total):
    pass

# Après : Objet paramètre
def creer_facture(client, adresse, commande):
    pass

"""
[ATTENTION] RÈGLES SÉCURITÉ REFACTORING


RÈGLE 1 : Tests verts AVANT de commencer
─────────────────────────────────────────
"""

# [OK] BON
def refactorer():
    # 1. Tous les tests passent
    run_tests()  # [OK] All green
    
    # 2. Refactorer
    ameliorer_code()
    
    # 3. Vérifier tests toujours verts
    run_tests()  # [OK] Still green

# [X] MAUVAIS
def refactorer_mal():
    run_tests()  # [X] Some tests fail
    # STOP ! Ne pas refactorer avec tests rouges !

"""
RÈGLE 2 : Baby steps (petits pas)
──────────────────────────────────
"""

# [OK] BON : Un refactoring à la fois
1. Renommer variable -> Run tests [OK]
2. Extraire méthode -> Run tests [OK]
3. Simplifier condition -> Run tests [OK]

# [X] MAUVAIS : Tout en même temps
1. Renommer + Extraire + Simplifier + Réorganiser
2. Run tests -> [X] Fail !
3. Quelle modification a cassé ? [SHRUG][MALE_SIGN]

"""
RÈGLE 3 : Ne pas ajouter de fonctionnalité
───────────────────────────────────────────
"""

# [BLEU] REFACTOR : Seulement améliorer
def calculer_total(items):
    # Améliorer structure
    return sum(item.price for item in items)

# [X] Pas de nouvelle fonctionnalité pendant refactor !
def calculer_total(items):
    total = sum(item.price for item in items)
    # [X] Nouvelle feature (taxes) pendant refactor !
    taxes = total * 0.2
    return total + taxes

# [OK] Nouvelle feature = Nouveau cycle RED-GREEN-REFACTOR

"""
RÈGLE 4 : Commit après chaque refactoring réussi
─────────────────────────────────────────────────
"""

# Git workflow avec TDD
git commit -m "[ROUGE] RED: Add test for user creation"
git commit -m "[VERT] GREEN: Implement user creation"
git commit -m "[BLEU] REFACTOR: Extract validation method"

# Avantage : Facile de revert si refactoring échoue

"""
QUAND S'ARRÊTER DE REFACTORER ?
────────────────────────────────

[BLACK_SQUARE_FOR_STOP] S'arrêter quand :
- Code est suffisamment clair
- Pas de duplication évidente
- Tests toujours verts
- Time-box dépassé (5-10 min max)

[IDEE] Refactoring = Itératif
Pas besoin de perfection immédiate
On reviendra plus tard si nécessaire


REFACTORING DES TESTS AUSSI !
──────────────────────────────
"""

# Tests peuvent aussi avoir duplication

# Avant : Duplication dans les tests
def test_user_creation_alice():
    db = Database()
    service = UserService(db)
    user = service.create_user("alice@example.com")
    assert user.email == "alice@example.com"

def test_user_creation_bob():
    db = Database()
    service = UserService(db)
    user = service.create_user("bob@example.com")
    assert user.email == "bob@example.com"

# Après : Setup partagé (fixture)
@pytest.fixture
def user_service():
    db = Database()
    return UserService(db)

def test_user_creation_alice(user_service):
    user = user_service.create_user("alice@example.com")
    assert user.email == "alice@example.com"

def test_user_creation_bob(user_service):
    user = user_service.create_user("bob@example.com")
    assert user.email == "bob@example.com"

"""
[ATTENTION] Attention : Tests doivent rester LISIBLES
Balance entre DRY et clarté
"""

# ----------------------------------------------------------------------------
# [SYNC] LE CYCLE COMPLET EN ACTION
# ----------------------------------------------------------------------------

"""
EXEMPLE COMPLET : PANIER D'ACHAT

Fonctionnalités à implémenter :
1. Ajouter article
2. Calculer total
3. Appliquer remise
4. Vider panier


═══════════════════════════════════════════════════════════════
ITÉRATION 1 : Ajouter article
═══════════════════════════════════════════════════════════════

[ROUGE] RED
──────
"""

def test_ajouter_article():
    panier = Panier()
    panier.ajouter(Article("Livre", 10))
    assert len(panier.articles) == 1

# Lancer -> NameError: name 'Panier' is not defined [OK]

"""
[VERT] GREEN
────────
"""

class Panier:
    def __init__(self):
        self.articles = []
    
    def ajouter(self, article):
        self.articles.append(article)

class Article:
    def __init__(self, nom, prix):
        self.nom = nom
        self.prix = prix

# Lancer tests -> [OK] Passe !

"""
[BLEU] REFACTOR
───────────
"""

# Code déjà propre, pas de refactoring nécessaire
# On continue !

"""
═══════════════════════════════════════════════════════════════
ITÉRATION 2 : Calculer total
═══════════════════════════════════════════════════════════════

[ROUGE] RED
──────
"""

def test_total_panier_vide():
    panier = Panier()
    assert panier.total() == 0

# Lancer -> AttributeError: no method 'total' [OK]

"""
[VERT] GREEN
────────
"""

class Panier:
    def __init__(self):
        self.articles = []
    
    def ajouter(self, article):
        self.articles.append(article)
    
    def total(self):
        return 0  # Fake it !

# Lancer tests -> [OK] Tous passent !

"""
[ROUGE] RED (encore)
───────────────
"""

def test_total_un_article():
    panier = Panier()
    panier.ajouter(Article("Livre", 10))
    assert panier.total() == 10

# Lancer -> AssertionError: 0 != 10 [OK]

"""
[VERT] GREEN
────────
"""

class Panier:
    def __init__(self):
        self.articles = []
    
    def ajouter(self, article):
        self.articles.append(article)
    
    def total(self):
        return sum(article.prix for article in self.articles)

# Lancer tests -> [OK] Tous passent !

"""
[BLEU] REFACTOR
───────────
"""

# Code propre, on continue !

"""
═══════════════════════════════════════════════════════════════
ITÉRATION 3 : Appliquer remise
═══════════════════════════════════════════════════════════════

[ROUGE] RED
──────
"""

def test_appliquer_remise_pourcentage():
    panier = Panier()
    panier.ajouter(Article("Livre", 100))
    panier.appliquer_remise(20)  # 20%
    assert panier.total() == 80

# Lancer -> AttributeError: no method 'appliquer_remise' [OK]

"""
[VERT] GREEN
────────
"""

class Panier:
    def __init__(self):
        self.articles = []
        self.remise = 0
    
    def ajouter(self, article):
        self.articles.append(article)
    
    def appliquer_remise(self, pourcentage):
        self.remise = pourcentage
    
    def total(self):
        sous_total = sum(article.prix for article in self.articles)
        return sous_total * (1 - self.remise / 100)

# Lancer tests -> [OK] Tous passent !

"""
[BLEU] REFACTOR
───────────
"""

# Extraire calcul remise
class Panier:
    def __init__(self):
        self.articles = []
        self.remise = 0
    
    def ajouter(self, article):
        self.articles.append(article)
    
    def appliquer_remise(self, pourcentage):
        self.remise = pourcentage
    
    def total(self):
        sous_total = self._calculer_sous_total()
        return self._appliquer_remise_au_total(sous_total)
    
    def _calculer_sous_total(self):
        return sum(article.prix for article in self.articles)
    
    def _appliquer_remise_au_total(self, montant):
        return montant * (1 - self.remise / 100)

# Lancer tests -> [OK] Tous passent !

"""
═══════════════════════════════════════════════════════════════
RÉSULTAT FINAL
═══════════════════════════════════════════════════════════════

Code propre, testé, fonctionnel :
"""

class Panier:
    def __init__(self):
        self.articles = []
        self.remise = 0
    
    def ajouter(self, article):
        self.articles.append(article)
    
    def appliquer_remise(self, pourcentage):
        if not 0 <= pourcentage <= 100:
            raise ValueError("Remise doit être entre 0 et 100")
        self.remise = pourcentage
    
    def total(self):
        sous_total = self._calculer_sous_total()
        return self._appliquer_remise_au_total(sous_total)
    
    def _calculer_sous_total(self):
        return sum(article.prix for article in self.articles)
    
    def _appliquer_remise_au_total(self, montant):
        return montant * (1 - self.remise / 100)

"""
[IDEE] LEÇONS DE CET EXEMPLE

1. Progression INCRÉMENTALE
   Panier vide -> 1 article -> Multiple -> Remise

2. Tests GUIDENT le design
   API émergente naturellement

3. Chaque cycle = 2-5 minutes
   Feedback constant

4. Refactoring SÉCURISÉ
   Tests protègent contre régressions

5. Code final = SIMPLE et CLAIR
   Pas de sur-ingénierie
"""

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Phase RED en détail
   - Choisir micro-fonctionnalité
   - Écrire test comme si code existait
   - Vérifier échec pour bonne raison
   - Éviter pièges courants

[OK] Phase GREEN en détail
   - Fake it till you make it
   - Code MINIMAL
   - 3 stratégies : Fake, Obvious, Triangulation
   - Éviter sur-ingénierie

[OK] Phase REFACTOR en détail
   - Quand refactorer (signaux)
   - Types refactoring courants
   - Règles de sécurité
   - Refactoring des tests aussi

[OK] Cycle complet en action
   - Exemple panier d'achat
   - 3-4 itérations
   - Design émergent progressif


[CLE] POINTS CLÉS

1. Cycle RED-GREEN-REFACTOR = Discipline
   Respecter ordre et tempo

2. Chaque phase a son objectif
   RED : Quoi faire
   GREEN : Le faire marcher
   REFACTOR : Le faire bien

3. Baby steps
   Petits pas = Feedback rapide

4. Tests toujours verts
   Sauf pendant RED !

5. Refactoring n'est pas optionnel
   Partie intégrante du TDD


[THOUGHT_BALLOON] CITATIONS

"Make it work, make it right, make it fast."
- Kent Beck

"Red, green, refactor. Repeat."
- Cycle TDD en 4 mots


[RAPIDE] PROCHAINE ÉTAPE

Chapitre 3 : Mythes et réalités du TDD
Démystifions les idées reçues !
"""

# ============================================================================
# FIN CHAPITRE 2 - SUITE DANS MÊME FICHIER
# ============================================================================


# ============================================================================
# [LIVRE] TDD - PARTIE 0 (FIN) : MYTHES ET RÉALITÉS
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 3 : MYTHES ET RÉALITÉS DU TDD
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Identifier les mythes courants sur TDD
[OK] Comprendre les vraies difficultés
[OK] Répondre aux objections
[OK] Éviter les pièges débutants
[OK] Avoir des attentes réalistes
"""

# ----------------------------------------------------------------------------
# [X] MYTHE 1 : "TDD RALENTIT LE DÉVELOPPEMENT"
# ----------------------------------------------------------------------------

"""
LE MYTHE
────────

"Écrire des tests prend du temps.
TDD double le temps de développement.
On n'a pas le temps pour ça."


LA RÉALITÉ
──────────

[OK] TDD ralentit AU DÉBUT (court terme)
   Semaines 1-4 : +30-50% temps
   - Courbe d'apprentissage
   - Nouveaux réflexes à acquérir
   - Pratique nécessaire

[OK] TDD ACCÉLÈRE ensuite (moyen/long terme)
   Après 2-3 mois : Gain net de temps
   - Moins de bugs (-40 à -90%)
   - Moins de debugging (-50 à -70%)
   - Refactoring serein
   - Pas de régression


GRAPHIQUE RÉALISTE
──────────────────
"""

"""
    Temps de développement
    ^
    |           Sans TDD
    |          ╱
    |        ╱
    |      ╱
    |    ╱   ╲___    Avec TDD
    |  ╱          ───────────
    |╱__________________ 
    0  3  6  9  12      Mois ->

Semaines 1-12 : TDD plus lent
Après 3 mois  : TDD plus rapide
Après 6 mois  : Différence significative
"""

"""
ÉTUDE DE CAS RÉELLE
───────────────────

Équipe A (Sans TDD) :
├─ Feature 1 : 2 jours
├─ Bug fixing : 3 jours  [!]
├─ Feature 2 : 3 jours
├─ Bug fixing : 4 jours  [!]
├─ Feature 3 : 4 jours
└─ Total : 16 jours

Équipe B (Avec TDD) :
├─ Feature 1 : 3 jours (+50%)
├─ Bug fixing : 0.5 jour [OK]
├─ Feature 2 : 3 jours
├─ Bug fixing : 0.3 jour [OK]
├─ Feature 3 : 3 jours
└─ Total : 9.8 jours (-40% !)


[IDEE] VÉRITÉ

TDD = Investissement rentable
Coût initial < Gains cumulatifs
"""

# ----------------------------------------------------------------------------
# [X] MYTHE 2 : "TDD REMPLACE LE DESIGN"
# ----------------------------------------------------------------------------

"""
LE MYTHE
────────

"Avec TDD, pas besoin de design.
Les tests font émerger le design automatiquement.
On code sans réfléchir."


LA RÉALITÉ
──────────

[X] TDD ≠ Pas de design
[OK] TDD = Design émergent + Design intentionnel


TDD GUIDE le design, pas le REMPLACE
─────────────────────────────────────

1. AVANT TDD : Réflexion de haut niveau
"""

# Penser architecture globale
"""
Système de commandes :
- Service de validation
- Service de paiement
- Service de notification
- Repository pour persistance

v Architecture macro définie
"""

"""
2. PENDANT TDD : Design détaillé émerge
"""

# Tests guident les détails
def test_valider_commande_complete():
    # Test force à définir API
    commande = Commande(...)
    validateur = ValidateurCommande()
    
    # API claire émerge des tests
    resultat = validateur.valider(commande)
    assert resultat.est_valide

"""
3. REFACTOR : Design s'améliore
"""

# Refactoring améliore structure
class ValidateurCommande:
    def __init__(self, regles_validation):
        # Injection dépendances émerge
        self.regles = regles_validation
    
    def valider(self, commande):
        # Design SOLID émerge naturellement
        return all(
            regle.appliquer(commande)
            for regle in self.regles
        )

"""
[IDEE] VÉRITÉ

TDD = Outil de design, pas solution magique
Toujours réfléchir à l'architecture !


QUAND FAIRE DU "BIG DESIGN" ?
──────────────────────────────

[OK] Faire du design AVANT :
- Architecture globale
- Choix technologiques majeurs
- Patterns généraux
- Interfaces principales

[OK] Laisser émerger PENDANT :
- Détails d'implémentation
- Structures de données exactes
- Méthodes privées
- Optimisations
"""

# ----------------------------------------------------------------------------
# [X] MYTHE 3 : "100% COUVERTURE = 0 BUG"
# ----------------------------------------------------------------------------

"""
LE MYTHE
────────

"Avec TDD, j'ai 100% de couverture de code.
Donc mon code est parfait, sans bugs."


LA RÉALITÉ
──────────

[X] 100% coverage ≠ Code parfait
[OK] 100% coverage = Chaque ligne exécutée, pas testée correctement


EXEMPLE TROMPEUR
────────────────
"""

# Code avec 100% coverage
def diviser(a, b):
    return a / b  # <- Ligne exécutée

# Test (mauvais)
def test_diviser():
    resultat = diviser(10, 2)
    assert resultat == 5

# Coverage : 100% [OK]
# Bugs non détectés :
# - diviser(10, 0) -> ZeroDivisionError
# - diviser("10", 2) -> TypeError
# - diviser(None, 2) -> TypeError

"""
[IDEE] VÉRITÉ

Coverage = Métrique utile mais insuffisante
100% coverage = Minimum, pas objectif final


CE QUE COVERAGE NE MESURE PAS
──────────────────────────────

[X] Qualité des assertions
   assert True  # <- Inutile mais compte

[X] Edge cases
   Cas limites non testés

[X] Logique métier complète
   Scénarios business manquants

[X] Intégrations
   Interactions entre composants

[X] Performance
   Code lent non détecté


CE QUI COMPTE VRAIMENT
───────────────────────

[OK] Tests significatifs
   Testent comportement réel

[OK] Cas limites couverts
   Edge cases, erreurs, nulls

[OK] Tests maintenables
   Faciles à lire et modifier

[OK] Tests rapides
   Feedback immédiat
"""

# ----------------------------------------------------------------------------
# [X] MYTHE 4 : "TDD = ÉCRIRE BEAUCOUP DE TESTS"
# ----------------------------------------------------------------------------

"""
LE MYTHE
────────

"TDD veut dire écrire un test pour chaque ligne de code.
Il faut des milliers de tests."


LA RÉALITÉ
──────────

[X] TDD ≠ Tester tout
[OK] TDD = Tester comportement important


RÈGLE 80/20
───────────

80% de la valeur avec 20% des tests

Tester :
[OK] Comportement public
[OK] Logique métier
[OK] Cas limites importants
[OK] Intégrations critiques

Ne PAS tester :
[X] Getters/setters triviaux
[X] Constructeurs simples
[X] Code généré automatiquement
[X] Frameworks (déjà testés)


EXEMPLE
───────
"""

class User:
    def __init__(self, email, name):
        self.email = email  # <- Pas besoin de tester
        self.name = name    # <- Pas besoin de tester
    
    def get_display_name(self):
        # [OK] TESTER : Logique métier
        if self.name:
            return self.name
        return self.email.split('@')[0]
    
    def is_admin(self):
        # [OK] TESTER : Comportement important
        return self.email.endswith('@admin.com')

# Tests nécessaires
def test_display_name_avec_nom():
    user = User("alice@example.com", "Alice")
    assert user.get_display_name() == "Alice"

def test_display_name_sans_nom():
    user = User("alice@example.com", None)
    assert user.get_display_name() == "alice"

def test_is_admin_pour_admin():
    user = User("alice@admin.com", "Alice")
    assert user.is_admin()

# PAS de test pour constructeur (trivial)

"""
[IDEE] VÉRITÉ

Qualité > Quantité
Tests ciblés sur comportement important
"""

# ----------------------------------------------------------------------------
# [X] MYTHE 5 : "TDD FONCTIONNE PARTOUT PAREIL"
# ----------------------------------------------------------------------------

"""
LE MYTHE
────────

"TDD s'applique de la même façon
quel que soit le contexte."


LA RÉALITÉ
──────────

[OK] TDD s'adapte au contexte

CONTEXTES DIFFÉRENTS
────────────────────

1. ALGORITHMES / LOGIQUE PURE
──────────────────────────────
"""

# TDD PARFAIT ici
def test_fibonacci():
    assert fibonacci(0) == 0
    assert fibonacci(1) == 1
    assert fibonacci(5) == 5
    assert fibonacci(10) == 55

# Cycle rapide : secondes
# Feedback immédiat
# Tests déterministes

"""
2. UI / INTERFACE GRAPHIQUE
────────────────────────────
"""

# TDD DIFFICILE ici
def test_bouton_bleu():
    # Comment tester la couleur ?
    # Comment tester l'alignement ?
    # Design itératif visuel
    pass

# [ATTENTION] Mieux : Tests E2E après validation design
# Ou : Tests d'intégration, pas unitaires

"""
3. BASE DE DONNÉES
──────────────────
"""

# TDD ADAPTÉ (avec précautions)
def test_creer_utilisateur():
    # Setup DB test
    db = get_test_database()
    
    # Test
    user = create_user(db, "alice@example.com")
    
    # Vérification
    assert user.email == "alice@example.com"
    
    # Cleanup
    db.rollback()

# [ATTENTION] Plus lent (I/O)
# Besoin de cleanup
# Tests parfois flaky

"""
4. APIs EXTERNES
────────────────
"""

# TDD AVEC MOCKS
def test_appeler_api_externe(mock_api):
    # Mock l'API externe
    mock_api.get_data.return_value = {"status": "ok"}
    
    # Test
    service = ExternalService(mock_api)
    result = service.fetch_data()
    
    # Vérification
    assert result["status"] == "ok"

# [ATTENTION] Tests d'intégration séparés (avec vraie API)

"""
[IDEE] VÉRITÉ

TDD = Flexible, pas dogmatique
Adapter au contexte
"""

# ----------------------------------------------------------------------------
# [X] MYTHE 6 : "TDD GARANTIT QUALITÉ"
# ----------------------------------------------------------------------------

"""
LE MYTHE
────────

"Si je fais du TDD, mon code sera forcément de qualité.
TDD = Assurance qualité automatique."


LA RÉALITÉ
──────────

[X] TDD ≠ Qualité garantie
[OK] TDD = AIDE à la qualité


TDD PEUT PRODUIRE MAUVAIS CODE
───────────────────────────────
"""

# Exemple : TDD mais code horrible
def test_calculator():
    c = Calculator()
    assert c.add(2, 3) == 5

class Calculator:
    # [X] Code horrible malgré TDD
    def add(self, a, b):
        x = a
        y = b
        z = 0
        while y > 0:
            z = z + 1
            y = y - 1
        while x > 0:
            z = z + 1
            x = x - 1
        return z

# Tests passent mais code est mauvais !

"""
CE QUE TDD NE GARANTIT PAS
──────────────────────────

[X] Code lisible
   (Dépend du développeur)

[X] Performance
   (Besoin de profiling)

[X] Sécurité
   (Besoin de tests spécifiques)

[X] UX
   (Besoin de tests utilisateurs)

[X] Architecture
   (Besoin de design intentionnel)


CE QUE TDD AIDE À OBTENIR
──────────────────────────

[OK] Code testable (découplé)
[OK] Moins de bugs
[OK] Refactoring sécurisé
[OK] Documentation vivante
[OK] Feedback rapide


[IDEE] VÉRITÉ

TDD = Outil puissant, pas baguette magique
Qualité = TDD + Skill + Experience
"""

# ----------------------------------------------------------------------------
# [X] MYTHE 7 : "TDD C'EST SEULEMENT POUR BACKEND"
# ----------------------------------------------------------------------------

"""
LE MYTHE
────────

"TDD ne marche que pour le backend.
Frontend, mobile, c'est trop compliqué."


LA RÉALITÉ
──────────

[OK] TDD fonctionne PARTOUT
Mais avec adaptations


TDD FRONTEND (React exemple)
─────────────────────────────
"""

# Test composant React
import { render, screen } from '@testing-library/react'

test('affiche le nom utilisateur', () => {
    // Arrange
    const user = { name: 'Alice' }
    
    // Act
    render(<UserProfile user={user} />)
    
    // Assert
    expect(screen.getByText('Alice')).toBeInTheDocument()
})

"""
TDD MOBILE (Flutter exemple)
─────────────────────────────
"""

testWidgets('affiche le compteur', (WidgetTester tester) async {
    // Arrange
    await tester.pumpWidget(MyApp());
    
    // Assert
    expect(find.text('0'), findsOneWidget);
    
    // Act
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
    
    // Assert
    expect(find.text('1'), findsOneWidget);
});

"""
TDD EMBEDDED (C exemple)
────────────────────────
"""

// Test unitaire sur microcontrôleur
void test_led_toggle() {
    // Arrange
    GPIO_Init();
    
    // Act
    LED_Toggle();
    
    // Assert
    assert(GPIO_Read(LED_PIN) == HIGH);
}

"""
[IDEE] VÉRITÉ

TDD = Universal
S'adapte à tout langage/plateforme
"""

# ----------------------------------------------------------------------------
# [OK] VRAIES DIFFICULTÉS DU TDD
# ----------------------------------------------------------------------------

"""
CHALLENGES RÉELS (et solutions)


DIFFICULTÉ 1 : COURBE D'APPRENTISSAGE
──────────────────────────────────────

Problème :
- Nouveaux réflexes à acquérir
- Frustration initiale
- Tentation d'abandonner

Solutions :
[OK] Pair programming avec expert
[OK] Katas de code (exercices)
[OK] Patience (2-3 mois)
[OK] Célébrer petites victoires


DIFFICULTÉ 2 : CODE LEGACY
───────────────────────────

Problème :
- Code existant non testé
- Couplage fort
- Difficile d'ajouter tests

Solutions :
[OK] Tester nouveau code
[OK] Ajouter tests avant modifications
[OK] Refactoring progressif
[OK] Pattern "Strangler Fig"


DIFFICULTÉ 3 : PRESSION TEMPORELLE
───────────────────────────────────

Problème :
- Deadlines serrées
- Management ne comprend pas
- Tentation de skip tests

Solutions :
[OK] Éduquer management (ROI)
[OK] Montrer gains (moins bugs)
[OK] Tests = filet sécurité pour aller vite
[OK] Technical debt = Dette à rembourser


DIFFICULTÉ 4 : TESTS FLAKY
───────────────────────────

Problème :
- Tests qui passent/échouent aléatoirement
- Perte de confiance
- Temps perdu

Solutions :
[OK] Isoler tests (pas d'état partagé)
[OK] Mocker I/O (réseau, temps, random)
[OK] Retry intelligents (si vraiment nécessaire)
[OK] Tests déterministes


DIFFICULTÉ 5 : TESTS LENTS
───────────────────────────

Problème :
- Tests prennent minutes/heures
- Feedback trop lent
- TDD impossible

Solutions :
[OK] Tests unitaires rapides (< 100ms)
[OK] Tests intégration séparés
[OK] Parallélisation
[OK] Tests sélectifs (changed files)
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] COMMENT RÉPONDRE AUX OBJECTIONS
# ----------------------------------------------------------------------------

"""
OBJECTIONS COURANTES ET RÉPONSES


OBJECTION 1 : "Pas le temps"
─────────────────────────────

[X] "On n'a pas le temps d'écrire des tests."

[OK] Réponse :
"On n'a pas le temps de NE PAS écrire de tests.
Le temps passé à debugger et corriger bugs
est bien supérieur au temps de TDD.

Données : -50% bugs = -70% debugging
-> Gain net de temps après 2-3 mois"


OBJECTION 2 : "Management ne veut pas"
───────────────────────────────────────

[X] "Mon manager dit que c'est du gaspillage."

[OK] Réponse :
"Présentez les bénéfices business :
- Moins de hotfixes en production
- Déploiements plus sûrs
- Vélocité stable (pas de slow-down)
- Onboarding plus rapide (tests = doc)

ROI : Rentable en 6-9 mois"


OBJECTION 3 : "Code trop complexe"
───────────────────────────────────

[X] "Mon code est trop complexe pour être testé."

[OK] Réponse :
"Si c'est difficile à tester, c'est difficile à utiliser.
TDD révèle problèmes de design.
-> Refactoring pour testabilité = meilleur design"


OBJECTION 4 : "Tests cassent quand je refactor"
────────────────────────────────────────────────

[X] "Mes tests cassent dès que je change le code."

[OK] Réponse :
"Tests couplés à l'implémentation, pas au comportement.
Tester interfaces publiques, pas détails internes.
Refactoring ne doit PAS casser tests."


OBJECTION 5 : "Marche pas pour mon domaine"
────────────────────────────────────────────

[X] "TDD ne marche pas dans mon domaine spécifique."

[OK] Réponse :
"TDD s'adapte à tout domaine.
Peut nécessiter ajustements (UI, temps-réel)
mais principes restent valides.
Adapter, ne pas abandonner."
"""

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] 7 mythes démystifiés
   1. TDD ralentit (faux long terme)
   2. TDD remplace design (non, le guide)
   3. 100% coverage = 0 bug (faux)
   4. Tester tout (non, le important)
   5. Même partout (non, s'adapte)
   6. Garantit qualité (aide, pas garantit)
   7. Seulement backend (non, partout)

[OK] Vraies difficultés
   - Courbe apprentissage
   - Code legacy
   - Pression temporelle
   - Tests flaky/lents

[OK] Comment répondre objections
   - Arguments rationnels
   - Données scientifiques
   - Bénéfices business


[CLE] POINTS CLÉS

1. TDD = Outil, pas miracle
   Comprendre limites et forces

2. Investissement rentable
   Court terme dur, long terme bénéfique

3. S'adapter au contexte
   Pragmatisme > Dogmatisme

4. Apprendre progressivement
   Patience et pratique

5. Éduquer l'équipe
   Partager connaissances


[THOUGHT_BALLOON] CITATIONS FINALES

"TDD is a discipline that works best when applied
intelligently, not dogmatically."
- Martin Fowler

"The only way to go fast is to go well."
- Uncle Bob Martin

"I'm not a great programmer;
I'm just a good programmer with great habits."
- Kent Beck


[RAPIDE] PROCHAINE ÉTAPE

PARTIE 1 : FONDAMENTAUX PRATIQUES !

Maintenant que vous comprenez :
- Ce qu'est le TDD (Chapitre 0)
- Pourquoi l'utiliser (Chapitre 1)
- Comment faire (Chapitre 2)
- Mythes et réalités (Chapitre 3)

Passons à la PRATIQUE ! [CODE]

-> tdd_partie1.md
"""

# ============================================================================
# [GRAPHIQUE] TABLEAU RÉCAPITULATIF PARTIE 0
# ============================================================================

"""
═══════════════════════════════════════════════════════════════════════════
RÉSUMÉ COMPLET PARTIE 0
═══════════════════════════════════════════════════════════════════════════

CHAPITRE 0 : QU'EST-CE QUE LE TDD ?
────────────────────────────────────
[OK] Définition : Écrire test AVANT code
[OK] Origine : Kent Beck (1999-2002)
[OK] Cycle : Red-Green-Refactor
[OK] Comparaisons : vs Test-Last, BDD, ATDD
[OK] Cas d'usage : Nouveau projet, algorithmes, refactoring


CHAPITRE 1 : POURQUOI LE TDD ?
───────────────────────────────
[OK] 7 bénéfices majeurs
   1. Qualité code supérieure
   2. Moins de bugs (-40 à -90%)
   3. Refactoring serein
   4. Documentation vivante
   5. Design émergent
   6. Feedback rapide
   7. Spécifications exécutables

[OK] Impact équipe : Collaboration, confiance, vélocité
[OK] ROI : Rentable en 6-9 mois


CHAPITRE 2 : LE CYCLE EN PROFONDEUR
────────────────────────────────────
[OK] Phase RED : Test qui échoue
   - Micro-fonctionnalité
   - API idéale
   - Échec bonne raison

[OK] Phase GREEN : Code minimal
   - Fake it
   - Obvious implementation
   - Triangulation

[OK] Phase REFACTOR : Amélioration
   - Éliminer duplication
   - Simplifier
   - Tests toujours verts


CHAPITRE 3 : MYTHES ET RÉALITÉS
────────────────────────────────
[OK] Mythes démystifiés
   - TDD ralentit (court terme oui, long terme non)
   - 100% coverage suffisant (non)
   - Garantit qualité (aide, pas garantit)

[OK] Vraies difficultés
   - Apprentissage (2-3 mois)
   - Legacy code
   - Pression temporelle

[OK] Objections courantes répondues


COMPÉTENCES ACQUISES
─────────────────────
[OBJECTIF] Compréhension profonde du TDD
[OBJECTIF] Cycle Red-Green-Refactor maîtrisé (théorie)
[OBJECTIF] Arguments pour défendre TDD
[OBJECTIF] Attentes réalistes
[OBJECTIF] Prêt pour la pratique


TEMPS INVESTI
──────────────
[TEMPS] Lecture : ~6-8 heures
[TEMPS] Réflexion : ~2-3 heures
[TEMPS] Total : ~8-11 heures


PROCHAINE ÉTAPE
───────────────
[LIVRE] PARTIE 1 : FONDAMENTAUX PRATIQUES
   - Setup environnement
   - Premier test TDD réel
   - Assertions et matchers
   - Types de tests

   Passons à la pratique ! [CODE]


═══════════════════════════════════════════════════════════════════════════
FÉLICITATIONS ! [BRAVO]

Vous avez complété la PARTIE 0 : INTRODUCTION ET PHILOSOPHIE

Vous comprenez maintenant :
[OK] Quoi : Qu'est-ce que le TDD
[OK] Pourquoi : Les bénéfices concrets
[OK] Comment : Le cycle Red-Green-Refactor
[OK] Mythes : Idées reçues démystifiées

Vous êtes prêt pour la pratique !
Continue avec tdd_partie1.md pour coder ! [RAPIDE]

═══════════════════════════════════════════════════════════════════════════
"""

# ============================================================================
# FIN DE LA PARTIE 0 : INTRODUCTION ET PHILOSOPHIE
# SUITE DANS tdd_partie1.md
# ============================================================================

# ============================================================================
# [LIVRE] TDD - PARTIE 1 : FONDAMENTAUX PRATIQUES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 4 : Setup Environnement
# - Chapitre 5 : Premier Test TDD
# - Chapitre 6 : Assertions et Matchers
# - Chapitre 7 : Types de Tests
#
# [TEMPS] TEMPS : ~6-8 heures
# [DOCS] PRÉREQUIS : Partie 0 complétée
# ============================================================================

"""
[OBJECTIF] OBJECTIFS DE LA PARTIE 1

À la fin de cette partie, vous saurez :
[OK] Configurer environnement TDD (Python, JavaScript, Java)
[OK] Écrire votre premier test TDD
[OK] Utiliser assertions efficacement
[OK] Distinguer types de tests
[OK] Organiser structure de tests
[OK] Lancer tests automatiquement
"""

# ============================================================================
# [GUIDE] CHAPITRE 4 : SETUP ENVIRONNEMENT
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Choisir framework de test adapté
[OK] Installer et configurer outils
[OK] Structurer projet pour TDD
[OK] Lancer tests en ligne de commande
[OK] Intégrer avec IDE
"""

# ----------------------------------------------------------------------------
# [PYTHON] SETUP PYTHON (pytest)
# ----------------------------------------------------------------------------

"""
POURQUOI pytest ?

pytest = Framework de test le plus populaire Python
[OK] Syntaxe simple et pythonique
[OK] Découverte automatique des tests
[OK] Fixtures puissantes
[OK] Plugins riches
[OK] Assertions natives Python


INSTALLATION
────────────
"""

# Créer environnement virtuel (recommandé)
python -m venv venv

# Activer
# Windows
venv\Scripts\activate
# Mac/Linux
source venv/bin/activate

# Installer pytest
pip install pytest

# Installer plugins utiles
pip install pytest-cov      # Coverage
pip install pytest-watch    # Auto-rerun
pip install pytest-xdist    # Parallélisation

# Vérifier installation
pytest --version
# pytest 7.4.3

"""
STRUCTURE DE PROJET
───────────────────
"""

mon_projet/
├── venv/                   # Environnement virtuel
├── src/                    # Code de production
│   ├── __init__.py
│   ├── calculatrice.py
│   └── panier.py
├── tests/                  # Tests
│   ├── __init__.py
│   ├── test_calculatrice.py
│   └── test_panier.py
├── pytest.ini              # Configuration pytest
├── .coveragerc             # Configuration coverage
└── requirements.txt        # Dépendances

"""
[IDEE] CONVENTIONS PYTEST

Pytest découvre automatiquement :
[OK] Fichiers : test_*.py ou *_test.py
[OK] Classes : Test*
[OK] Fonctions : test_*

Exemples valides :
[OK] test_calculatrice.py
[OK] test_mon_module.py
[OK] calculatrice_test.py

Exemples invalides :
[X] tests.py (pas de préfixe test_)
[X] my_test.py (pas test_ au début)


CONFIGURATION pytest.ini
─────────────────────────
"""

# pytest.ini
"""
[pytest]
# Où chercher les tests
testpaths = tests

# Pattern des fichiers de test
python_files = test_*.py *_test.py

# Pattern des classes de test
python_classes = Test*

# Pattern des fonctions de test
python_functions = test_*

# Options par défaut
addopts = 
    -v                      # Verbose
    --tb=short             # Traceback court
    --strict-markers       # Markers stricts
    --cov=src              # Coverage du dossier src
    --cov-report=html      # Rapport HTML
    --cov-report=term      # Rapport terminal
"""

"""
PREMIER TEST SIMPLE
───────────────────
"""

# tests/test_premier.py
def test_addition():
    """Mon premier test pytest"""
    resultat = 2 + 2
    assert resultat == 4

"""
LANCER LES TESTS
────────────────
"""

# Tous les tests
pytest

# Avec verbose
pytest -v

# Test spécifique
pytest tests/test_calculatrice.py

# Fonction spécifique
pytest tests/test_calculatrice.py::test_addition

# Pattern
pytest -k "test_addition"

# Avec coverage
pytest --cov=src --cov-report=html

# Watch mode (relance auto)
pytest-watch

"""
[IDEE] OUTPUT pytest

$ pytest -v

tests/test_premier.py::test_addition PASSED    [100%]

============== 1 passed in 0.01s ==============


INTÉGRATION IDE
───────────────

VS Code :
1. Installer extension "Python"
2. Cmd+Shift+P -> "Python: Configure Tests"
3. Sélectionner pytest
4. Icônes [BLACK_RIGHT-POINTING_TRIANGLE] apparaissent dans les tests

PyCharm :
1. File -> Settings -> Tools -> Python Integrated Tools
2. Default test runner : pytest
3. Clic droit sur test -> Run/Debug
"""

# ----------------------------------------------------------------------------
# [JAUNE] SETUP JAVASCRIPT (Jest)
# ----------------------------------------------------------------------------

"""
POURQUOI Jest ?

Jest = Framework de test populaire JavaScript
[OK] Zero config (pour projets React)
[OK] Snapshots
[OK] Mocking intégré
[OK] Coverage intégré
[OK] Fast et parallèle


INSTALLATION
────────────
"""

# Nouveau projet
npm init -y

# Installer Jest
npm install --save-dev jest

# Installer types TypeScript (optionnel)
npm install --save-dev @types/jest

# Configuration package.json
"""
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}
"""

"""
STRUCTURE DE PROJET
───────────────────
"""

mon_projet/
├── node_modules/
├── src/
│   ├── calculatrice.js
│   └── panier.js
├── __tests__/              # Tests (alternative)
│   ├── calculatrice.test.js
│   └── panier.test.js
├── src/
│   ├── calculatrice.js
│   ├── calculatrice.test.js  # Tests à côté du code
│   ├── panier.js
│   └── panier.test.js
├── jest.config.js          # Configuration
└── package.json

"""
[IDEE] CONVENTIONS Jest

Jest découvre automatiquement :
[OK] Fichiers dans __tests__/
[OK] Fichiers *.test.js ou *.spec.js
[OK] Fichiers *.test.ts (TypeScript)


CONFIGURATION jest.config.js
─────────────────────────────
"""

// jest.config.js
module.exports = {
  // Environnement
  testEnvironment: 'node',  // ou 'jsdom' pour React
  
  // Pattern des tests
  testMatch: [
    '**/__tests__/**/*.[jt]s?(x)',
    '**/?(*.)+(spec|test).[jt]s?(x)'
  ],
  
  // Coverage
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.test.{js,jsx,ts,tsx}'
  ],
  
  // Threshold coverage
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

"""
PREMIER TEST SIMPLE
───────────────────
"""

// src/calculatrice.test.js
test('addition de 2 + 2 égale 4', () => {
  const resultat = 2 + 2;
  expect(resultat).toBe(4);
});

"""
LANCER LES TESTS
────────────────
"""

# Tous les tests
npm test

# Watch mode
npm run test:watch

# Coverage
npm run test:coverage

# Test spécifique
npm test -- calculatrice.test.js

"""
[IDEE] OUTPUT Jest

 PASS  src/calculatrice.test.js
  [OK] addition de 2 + 2 égale 4 (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
"""

# ----------------------------------------------------------------------------
# [HOT_BEVERAGE] SETUP JAVA (JUnit 5)
# ----------------------------------------------------------------------------

"""
POURQUOI JUnit 5 ?

JUnit 5 = Standard de facto pour Java
[OK] Annotations modernes (@Test, @BeforeEach)
[OK] Assertions riches
[OK] Parameterized tests
[OK] Extensions
[OK] Intégration IDE excellente


INSTALLATION (Maven)
────────────────────
"""

<!-- pom.xml -->
"""
<dependencies>
    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!-- Maven Surefire pour lancer tests -->
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0</version>
        </plugin>
    </plugins>
</build>
"""

"""
INSTALLATION (Gradle)
─────────────────────
"""

// build.gradle
"""
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}

test {
    useJUnitPlatform()
}
"""

"""
STRUCTURE DE PROJET
───────────────────
"""

mon_projet/
├── src/
│   ├── main/
│   │   └── java/
│   │       └── com/
│   │           └── example/
│   │               ├── Calculatrice.java
│   │               └── Panier.java
│   └── test/
│       └── java/
│           └── com/
│               └── example/
│                   ├── CalculatriceTest.java
│                   └── PanierTest.java
└── pom.xml

"""
[IDEE] CONVENTIONS JUnit

[OK] Package test = Package prod
[OK] Classe de test = ClasseTest
[OK] Méthode de test = testMethode() ou methode()
   avec @Test


PREMIER TEST SIMPLE
───────────────────
"""

// src/test/java/com/example/CalculatriceTest.java
package com.example;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatriceTest {
    
    @Test
    void additionDeuxNombres() {
        int resultat = 2 + 2;
        assertEquals(4, resultat);
    }
}

"""
LANCER LES TESTS
────────────────
"""

# Maven
mvn test

# Gradle
./gradlew test

# Test spécifique (Maven)
mvn test -Dtest=CalculatriceTest

# Test spécifique (Gradle)
./gradlew test --tests CalculatriceTest

"""
[IDEE] OUTPUT JUnit

[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] BUILD SUCCESS
"""

# ----------------------------------------------------------------------------
# [OUTIL] OUTILS COMPLÉMENTAIRES
# ----------------------------------------------------------------------------

"""
COVERAGE (TOUS LANGAGES)
────────────────────────

Python : pytest-cov
    pytest --cov=src --cov-report=html
    -> htmlcov/index.html

JavaScript : Jest intégré
    npm run test:coverage
    -> coverage/lcov-report/index.html

Java : JaCoCo
    mvn jacoco:report
    -> target/site/jacoco/index.html


WATCH MODE (TOUS LANGAGES)
───────────────────────────

Python : pytest-watch
    ptw

JavaScript : Jest
    npm run test:watch

Java : IntelliJ IDEA
    Run with Coverage -> Auto-rerun


CI/CD INTÉGRATION
─────────────────

GitHub Actions (exemple Python) :
"""

# .github/workflows/tests.yml
"""
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
        with:
          python-version: '3.10'
      - run: pip install -r requirements.txt
      - run: pytest --cov=src
"""

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Setup Python (pytest)
   - Installation et configuration
   - Structure projet
   - Lancement tests
   
[OK] Setup JavaScript (Jest)
   - Installation et configuration
   - Conventions nommage
   - Watch mode
   
[OK] Setup Java (JUnit 5)
   - Maven/Gradle
   - Annotations
   - IDE intégration

[OK] Outils complémentaires
   - Coverage
   - Watch mode
   - CI/CD


[CLE] POINTS CLÉS

1. Conventions de nommage importantes
   test_* ou *_test pour Python
   *.test.js pour JavaScript
   *Test.java pour Java

2. Configuration projet dès le début
   pytest.ini, jest.config.js, pom.xml

3. Tests à côté ou séparés du code
   Les deux approches sont valides

4. Watch mode = Productivité
   Feedback immédiat pendant développement

5. Coverage = Métrique utile
   Viser 80%+ mais qualité > quantité


[RAPIDE] PRÊT POUR LA PRATIQUE

Environnement configuré [OK]
Passons au premier test TDD ! ->
"""


# ============================================================================
# [GUIDE] CHAPITRE 5 : PREMIER TEST TDD
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Écrire premier test TDD complet
[OK] Suivre cycle Red-Green-Refactor
[OK] Éviter erreurs débutants
[OK] Maintenir rythme TDD
[OK] Développer réflexes TDD
"""

# ----------------------------------------------------------------------------
# [DEMARRAGE] KATA TDD : CALCULATRICE
# ----------------------------------------------------------------------------

"""
KATA = Exercice de programmation pour pratiquer

Nous allons créer une calculatrice simple en TDD pur.

Fonctionnalités :
1. Addition
2. Soustraction
3. Multiplication
4. Division
5. Gestion erreurs


[IDEE] RÈGLES DU JEU

1. Écrire le test AVANT le code
2. Faire le test échouer (RED)
3. Écrire code minimal (GREEN)
4. Améliorer (REFACTOR)
5. Répéter


═══════════════════════════════════════════════════════════════
ITÉRATION 1 : ADDITION DE DEUX NOMBRES
═══════════════════════════════════════════════════════════════

[ROUGE] RED : Écrire le test
───────────────────────
"""

# tests/test_calculatrice.py
def test_additionner_deux_nombres():
    """
    [IDEE] PREMIÈRE QUESTION : Qu'est-ce qu'on teste ?
    
    On veut une calculatrice qui additionne deux nombres.
    API idéale : calc.additionner(2, 3) -> 5
    """
    calc = Calculatrice()
    resultat = calc.additionner(2, 3)
    assert resultat == 5

# Lancer : pytest tests/test_calculatrice.py

"""
Output :
────────
tests/test_calculatrice.py::test_additionner_deux_nombres FAILED

E   NameError: name 'Calculatrice' is not defined

[OK] Test échoue pour la BONNE raison !
   (classe n'existe pas)


[VERT] GREEN : Code minimal
───────────────────────
"""

# src/calculatrice.py
class Calculatrice:
    def additionner(self, a, b):
        return 5  # [!] Oui, vraiment !

"""
[IDEE] POURQUOI 5 ?

C'est la solution la PLUS SIMPLE qui fait passer le test.
On généralise seulement quand tests FORCENT.


Lancer : pytest tests/test_calculatrice.py

Output :
────────
tests/test_calculatrice.py::test_additionner_deux_nombres PASSED

[OK] Test vert !


[BLEU] REFACTOR : Améliorer
───────────────────────

Code est simple, pas besoin de refactoring.
Passons au test suivant !


═══════════════════════════════════════════════════════════════
ITÉRATION 2 : GÉNÉRALISER L'ADDITION
═══════════════════════════════════════════════════════════════

[ROUGE] RED : Forcer la généralisation
─────────────────────────────────
"""

def test_additionner_autres_nombres():
    """Forcer implémentation réelle"""
    calc = Calculatrice()
    resultat = calc.additionner(10, 15)
    assert resultat == 25

"""
Lancer : pytest

Output :
────────
FAILED - AssertionError: assert 5 == 25

[OK] Test échoue (return 5 ne suffit plus)


[VERT] GREEN : Implémenter vraiment
───────────────────────────────
"""

class Calculatrice:
    def additionner(self, a, b):
        return a + b  # Vraie implémentation !

"""
Lancer : pytest

Output :
────────
test_additionner_deux_nombres PASSED
test_additionner_autres_nombres PASSED

[OK] Tous les tests passent !


[BLEU] REFACTOR : Pas nécessaire
────────────────────────────

Code déjà propre.


═══════════════════════════════════════════════════════════════
ITÉRATION 3 : SOUSTRACTION
═══════════════════════════════════════════════════════════════

[ROUGE] RED : Nouveau test
─────────────────────
"""

def test_soustraire_deux_nombres():
    calc = Calculatrice()
    resultat = calc.soustraire(10, 3)
    assert resultat == 7

"""
Output :
────────
AttributeError: 'Calculatrice' object has no attribute 'soustraire'

[OK] Bonne raison


[VERT] GREEN : Implémentation
─────────────────────────
"""

class Calculatrice:
    def additionner(self, a, b):
        return a + b
    
    def soustraire(self, a, b):
        return a - b  # Direct car évident

"""
[OK] Tests passent


═══════════════════════════════════════════════════════════════
ITÉRATION 4 : MULTIPLICATION
═══════════════════════════════════════════════════════════════

[ROUGE] RED
──────
"""

def test_multiplier_deux_nombres():
    calc = Calculatrice()
    resultat = calc.multiplier(4, 5)
    assert resultat == 20

"""
[VERT] GREEN
────────
"""

class Calculatrice:
    def additionner(self, a, b):
        return a + b
    
    def soustraire(self, a, b):
        return a - b
    
    def multiplier(self, a, b):
        return a * b

"""
═══════════════════════════════════════════════════════════════
ITÉRATION 5 : DIVISION
═══════════════════════════════════════════════════════════════

[ROUGE] RED
──────
"""

def test_diviser_deux_nombres():
    calc = Calculatrice()
    resultat = calc.diviser(10, 2)
    assert resultat == 5

"""
[VERT] GREEN
────────
"""

class Calculatrice:
    def additionner(self, a, b):
        return a + b
    
    def soustraire(self, a, b):
        return a - b
    
    def multiplier(self, a, b):
        return a * b
    
    def diviser(self, a, b):
        return a / b

"""
[BLEU] REFACTOR : Observer le code
───────────────────────────────

[THOUGHT_BALLOON] Y a-t-il de la duplication ?
   -> Non

[THOUGHT_BALLOON] Le code est-il clair ?
   -> Oui

[THOUGHT_BALLOON] Noms explicites ?
   -> Oui

[OK] Pas besoin de refactoring


═══════════════════════════════════════════════════════════════
ITÉRATION 6 : DIVISION PAR ZÉRO
═══════════════════════════════════════════════════════════════

[ROUGE] RED : Test d'erreur
──────────────────────
"""

import pytest

def test_diviser_par_zero_leve_erreur():
    """Division par zéro doit lever ValueError"""
    calc = Calculatrice()
    
    with pytest.raises(ValueError):
        calc.diviser(10, 0)

"""
[IDEE] TESTER LES EXCEPTIONS

pytest.raises(ExceptionType) vérifie qu'exception est levée


Output :
────────
FAILED - Did not raise ValueError

[OK] Bonne raison (pas d'exception levée)


[VERT] GREEN : Gérer erreur
───────────────────────
"""

class Calculatrice:
    def additionner(self, a, b):
        return a + b
    
    def soustraire(self, a, b):
        return a - b
    
    def multiplier(self, a, b):
        return a * b
    
    def diviser(self, a, b):
        if b == 0:
            raise ValueError("Division par zéro impossible")
        return a / b

"""
[OK] Test passe !


[BLEU] REFACTOR : Observer
──────────────────────

Code toujours propre.


═══════════════════════════════════════════════════════════════
ITÉRATION 7 : TESTER MESSAGE D'ERREUR
═══════════════════════════════════════════════════════════════

[ROUGE] RED : Test plus précis
─────────────────────────
"""

def test_diviser_par_zero_message_erreur():
    """Vérifier message d'erreur exact"""
    calc = Calculatrice()
    
    with pytest.raises(ValueError) as exc_info:
        calc.diviser(10, 0)
    
    assert str(exc_info.value) == "Division par zéro impossible"

"""
[OK] Passe déjà ! (implémentation OK)


═══════════════════════════════════════════════════════════════
CODE FINAL
═══════════════════════════════════════════════════════════════
"""

# src/calculatrice.py
class Calculatrice:
    """
    Calculatrice simple avec 4 opérations de base.
    
    Développée en TDD pur.
    """
    
    def additionner(self, a, b):
        """Additionne deux nombres."""
        return a + b
    
    def soustraire(self, a, b):
        """Soustrait b de a."""
        return a - b
    
    def multiplier(self, a, b):
        """Multiplie deux nombres."""
        return a * b
    
    def diviser(self, a, b):
        """
        Divise a par b.
        
        Raises:
            ValueError: Si b est zéro.
        """
        if b == 0:
            raise ValueError("Division par zéro impossible")
        return a / b

"""
# tests/test_calculatrice.py (complet)
"""

import pytest
from src.calculatrice import Calculatrice


class TestCalculatrice:
    """Tests de la classe Calculatrice"""
    
    def setup_method(self):
        """Exécuté avant chaque test"""
        self.calc = Calculatrice()
    
    # ADDITION
    def test_additionner_deux_nombres(self):
        assert self.calc.additionner(2, 3) == 5
    
    def test_additionner_autres_nombres(self):
        assert self.calc.additionner(10, 15) == 25
    
    def test_additionner_nombres_negatifs(self):
        assert self.calc.additionner(-5, -3) == -8
    
    # SOUSTRACTION
    def test_soustraire_deux_nombres(self):
        assert self.calc.soustraire(10, 3) == 7
    
    def test_soustraire_resultat_negatif(self):
        assert self.calc.soustraire(5, 10) == -5
    
    # MULTIPLICATION
    def test_multiplier_deux_nombres(self):
        assert self.calc.multiplier(4, 5) == 20
    
    def test_multiplier_par_zero(self):
        assert self.calc.multiplier(10, 0) == 0
    
    # DIVISION
    def test_diviser_deux_nombres(self):
        assert self.calc.diviser(10, 2) == 5
    
    def test_diviser_resultat_decimal(self):
        assert self.calc.diviser(10, 4) == 2.5
    
    def test_diviser_par_zero_leve_erreur(self):
        with pytest.raises(ValueError):
            self.calc.diviser(10, 0)
    
    def test_diviser_par_zero_message_erreur(self):
        with pytest.raises(ValueError) as exc_info:
            self.calc.diviser(10, 0)
        assert str(exc_info.value) == "Division par zéro impossible"

"""
Lancer tous les tests :
───────────────────────

$ pytest -v

tests/test_calculatrice.py::TestCalculatrice::test_additionner_deux_nombres PASSED
tests/test_calculatrice.py::TestCalculatrice::test_additionner_autres_nombres PASSED
tests/test_calculatrice.py::TestCalculatrice::test_additionner_nombres_negatifs PASSED
tests/test_calculatrice.py::TestCalculatrice::test_soustraire_deux_nombres PASSED
tests/test_calculatrice.py::TestCalculatrice::test_soustraire_resultat_negatif PASSED
tests/test_calculatrice.py::TestCalculatrice::test_multiplier_deux_nombres PASSED
tests/test_calculatrice.py::TestCalculatrice::test_multiplier_par_zero PASSED
tests/test_calculatrice.py::TestCalculatrice::test_diviser_deux_nombres PASSED
tests/test_calculatrice.py::TestCalculatrice::test_diviser_resultat_decimal PASSED
tests/test_calculatrice.py::TestCalculatrice::test_diviser_par_zero_leve_erreur PASSED
tests/test_calculatrice.py::TestCalculatrice::test_diviser_par_zero_message_erreur PASSED

============== 11 passed in 0.03s ==============

[OK] 100% tests passent !
[OK] Code développé ENTIÈREMENT en TDD
[OK] Chaque ligne testée
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] LEÇONS DU KATA
# ----------------------------------------------------------------------------

"""
CE QUE NOUS AVONS APPRIS

1. PROGRESSION INCRÉMENTALE
───────────────────────────

Nous n'avons PAS écrit :
[X] Tous les tests d'un coup
[X] Toute l'implémentation d'un coup

Nous avons fait :
[OK] Un test à la fois
[OK] Une fonctionnalité à la fois
[OK] Cycles courts (2-5 minutes)


2. FAKE IT TILL YOU MAKE IT
────────────────────────────

return 5 pour premier test
-> Forcé à généraliser par test suivant
-> Design émergent naturellement


3. TESTS GUIDENT L'API
──────────────────────

API claire émergée des tests :
- calc.additionner(a, b)
- calc.soustraire(a, b)
- etc.

Pas d'API complexe ou sur-engineered


4. GESTION ERREURS TESTÉE
──────────────────────────

Division par zéro = cas limite
Testé explicitement avec pytest.raises()
Message d'erreur vérifié


5. REFACTORING SÉCURISÉ
───────────────────────

Chaque refactoring vérifié par tests
Confiance pour améliorer


6. DOCUMENTATION VIVANTE
────────────────────────

Tests = Spécification exécutable
- Comment utiliser calculatrice
- Comportements attendus
- Cas limites


7. COUVERTURE NATURELLE
────────────────────────

100% coverage sans y penser
Résultat naturel du TDD
"""

# ----------------------------------------------------------------------------
# [ATTENTION] ERREURS COURANTES DÉBUTANTS
# ----------------------------------------------------------------------------

"""
ERREUR 1 : Écrire plusieurs tests d'un coup
────────────────────────────────────────────
"""

# [X] MAUVAIS
def test_toutes_operations():
    calc = Calculatrice()
    # Test tout en même temps
    assert calc.additionner(2, 3) == 5
    assert calc.soustraire(5, 2) == 3
    assert calc.multiplier(2, 3) == 6
    assert calc.diviser(6, 2) == 3

# [OK] BON : Un test = Un comportement
def test_additionner():
    calc = Calculatrice()
    assert calc.additionner(2, 3) == 5

def test_soustraire():
    calc = Calculatrice()
    assert calc.soustraire(5, 2) == 3

"""
ERREUR 2 : Implémenter avant test
──────────────────────────────────
"""

# [X] MAUVAIS : Coder d'abord
class Calculatrice:
    def additionner(self, a, b):
        return a + b
    
    def soustraire(self, a, b):
        return a - b
    # ...

# Puis écrire tests après
# -> Plus du TDD !

# [OK] BON : Test TOUJOURS avant

"""
ERREUR 3 : Sur-implémenter en phase GREEN
──────────────────────────────────────────
"""

# Test
def test_additionner():
    calc = Calculatrice()
    assert calc.additionner(2, 3) == 5

# [X] MAUVAIS : Trop de code
class Calculatrice:
    def __init__(self):
        self.historique = []
        self.cache = {}
    
    def additionner(self, a, b):
        # Vérifie cache
        key = f"{a}+{b}"
        if key in self.cache:
            return self.cache[key]
        
        # Calcule
        result = a + b
        
        # Historique
        self.historique.append((a, b, result))
        
        # Cache
        self.cache[key] = result
        
        return result

# [OK] BON : Minimal
class Calculatrice:
    def additionner(self, a, b):
        return a + b

"""
ERREUR 4 : Sauter phase REFACTOR
─────────────────────────────────
"""

# Après plusieurs cycles, code peut être :
def calculer(op, a, b):
    if op == "add":
        return a + b
    elif op == "sub":
        return a - b
    elif op == "mul":
        return a * b
    elif op == "div":
        if b == 0:
            raise ValueError("Err")
        return a / b

# [X] Ne pas refactorer immédiatement

# [OK] BON : Refactorer après GREEN
class Calculatrice:
    def calculer(self, operation, a, b):
        operations = {
            'add': self._additionner,
            'sub': self._soustraire,
            'mul': self._multiplier,
            'div': self._diviser
        }
        
        if operation not in operations:
            raise ValueError(f"Opération inconnue: {operation}")
        
        return operations[operation](a, b)
    
    def _additionner(self, a, b):
        return a + b
    # ...

"""
ERREUR 5 : Tests trop couplés
──────────────────────────────
"""

# [X] MAUVAIS : Tests dépendent les uns des autres
calc = None

def test_1_creer_calculatrice():
    global calc
    calc = Calculatrice()

def test_2_additionner():
    # Dépend de test_1
    assert calc.additionner(2, 3) == 5

# [OK] BON : Tests indépendants
def test_additionner():
    calc = Calculatrice()  # Setup dans chaque test
    assert calc.additionner(2, 3) == 5

"""
ERREUR 6 : Ne pas lancer tests régulièrement
─────────────────────────────────────────────

[X] Écrire 5 tests puis lancer
[X] Coder 30 minutes puis tester

[OK] Lancer après CHAQUE modification
[OK] Cycle court : < 5 minutes
"""

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 1
# ----------------------------------------------------------------------------

"""
EXERCICE : PANIER D'ACHAT (TDD)

Créez une classe Panier en TDD pur.

Fonctionnalités à implémenter (dans l'ordre) :
1. Ajouter un article
2. Compter nombre d'articles
3. Calculer total
4. Vider le panier
5. Supprimer un article spécifique
6. Appliquer remise (pourcentage)

RÈGLES :
- Commencer par le test le plus simple
- Un test à la fois
- Respecter Red-Green-Refactor
- Pas de code sans test


SOLUTION (à faire APRÈS avoir essayé) :
═══════════════════════════════════════════════════════════════
"""

# Voir fichier séparé pour ne pas spoiler !

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Premier test TDD complet (Calculatrice)
   - 7 itérations
   - Cycle Red-Green-Refactor respecté
   - Code final propre et testé

[OK] Progression incrémentale
   - Un test à la fois
   - Fonctionnalité par fonctionnalité
   - Design émergent

[OK] Fake it till you make it
   - Solutions simples d'abord
   - Généralisation forcée par tests

[OK] Erreurs courantes évitées
   - Plusieurs tests d'un coup
   - Implémenter avant test
   - Sur-implémenter
   - Sauter refactor

[OK] Exercice pratique
   - Panier d'achat
   - Mise en pratique immédiate


[CLE] POINTS CLÉS

1. TDD = Discipline
   Respecter ordre strictement

2. Baby steps
   Petits pas = Succès

3. Tests = Design
   API émerge naturellement

4. Feedback constant
   Lancer tests après chaque modif

5. Pratique = Maîtrise
   Faire des katas régulièrement


[THOUGHT_BALLOON] CITATION

"The three rules of TDD:
1. Write no production code except to pass a failing test
2. Write only enough of a test to demonstrate a failure
3. Write only enough production code to pass the test"
- Uncle Bob Martin


[RAPIDE] PROCHAINE ÉTAPE

Chapitre 6 : Assertions et Matchers
Approfondir les vérifications dans les tests
"""

# ============================================================================
# FIN CHAPITRE 5 - SUITE DANS MÊME FICHIER
# ============================================================================

# ============================================================================
# [LIVRE] TDD - PARTIE 1 (SUITE) : ASSERTIONS ET TYPES DE TESTS
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 6 : ASSERTIONS ET MATCHERS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser toutes les assertions pytest
[OK] Écrire assertions claires et précises
[OK] Tester exceptions et erreurs
[OK] Assertions custom
[OK] Comparaisons complexes
"""

# ----------------------------------------------------------------------------
# [OK] ASSERTIONS DE BASE (pytest)
# ----------------------------------------------------------------------------

"""
ASSERTION SIMPLE
────────────────

Python utilise le mot-clé `assert` natif
pytest enrichit les messages d'erreur
"""

def test_assertions_basiques():
    # Égalité
    assert 2 + 2 == 4
    
    # Différence
    assert 3 != 4
    
    # Comparaisons
    assert 5 > 3
    assert 3 < 5
    assert 5 >= 5
    assert 3 <= 3
    
    # Booléens
    assert True
    assert not False
    
    # None
    value = None
    assert value is None
    assert value is not None  # Échouerait

"""
[IDEE] MESSAGES D'ERREUR pytest

Quand assertion échoue, pytest montre :
- Valeur attendue
- Valeur obtenue
- Contexte
"""

# Test
def test_exemple_echec():
    resultat = 2 + 2
    assert resultat == 5

# Output
"""
    def test_exemple_echec():
        resultat = 2 + 2
>       assert resultat == 5
E       assert 4 == 5

AssertionError
"""

# ----------------------------------------------------------------------------
# [TEXTE] ASSERTIONS SUR STRINGS
# ----------------------------------------------------------------------------

"""
ÉGALITÉ ET CONTENU
──────────────────
"""

def test_strings():
    text = "Hello World"
    
    # Égalité exacte
    assert text == "Hello World"
    
    # Contient
    assert "Hello" in text
    assert "Goodbye" not in text
    
    # Commence/Finit par
    assert text.startswith("Hello")
    assert text.endswith("World")
    
    # Case insensitive
    assert text.lower() == "hello world"

"""
REGEX (avec module re)
──────────────────────
"""

import re

def test_regex():
    email = "user@example.com"
    
    # Match pattern
    pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
    assert re.match(pattern, email)
    
    # Ou avec pytest.raises pour non-match
    invalid_email = "not-an-email"
    assert not re.match(pattern, invalid_email)

"""
MULTILINE
─────────
"""

def test_multiline_string():
    text = """
    Line 1
    Line 2
    Line 3
    """
    
    assert "Line 1" in text
    assert "Line 2" in text
    assert text.strip().startswith("Line 1")

# ----------------------------------------------------------------------------
# [LISTE] ASSERTIONS SUR COLLECTIONS
# ----------------------------------------------------------------------------

"""
LISTES
──────
"""

def test_listes():
    fruits = ["pomme", "banane", "orange"]
    
    # Longueur
    assert len(fruits) == 3
    
    # Contient élément
    assert "pomme" in fruits
    assert "kiwi" not in fruits
    
    # Index
    assert fruits[0] == "pomme"
    assert fruits[-1] == "orange"
    
    # Sous-liste
    assert fruits[0:2] == ["pomme", "banane"]
    
    # Ordre
    assert fruits == ["pomme", "banane", "orange"]
    
    # Présence sans ordre (set)
    assert set(fruits) == {"pomme", "banane", "orange"}

"""
VÉRIFIER VIDE/NON VIDE
──────────────────────
"""

def test_empty_collections():
    liste_vide = []
    liste_pleine = [1, 2, 3]
    
    # Vide
    assert not liste_vide
    assert len(liste_vide) == 0
    
    # Non vide
    assert liste_pleine
    assert len(liste_pleine) > 0

"""
DICTIONNAIRES
─────────────
"""

def test_dictionnaires():
    user = {
        "name": "Alice",
        "age": 25,
        "email": "alice@example.com"
    }
    
    # Clé existe
    assert "name" in user
    assert "phone" not in user
    
    # Valeur
    assert user["name"] == "Alice"
    assert user.get("age") == 25
    
    # Plusieurs clés
    assert set(user.keys()) == {"name", "age", "email"}
    
    # Sous-dictionnaire
    assert {"name": "Alice"}.items() <= user.items()

"""
SETS
────
"""

def test_sets():
    numbers = {1, 2, 3, 4, 5}
    
    # Contient
    assert 3 in numbers
    assert 10 not in numbers
    
    # Sous-ensemble
    assert {1, 2} <= numbers
    assert {1, 2}.issubset(numbers)
    
    # Intersection
    other = {3, 4, 5, 6}
    assert numbers & other == {3, 4, 5}
    
    # Union
    assert numbers | other == {1, 2, 3, 4, 5, 6}

# ----------------------------------------------------------------------------
# [OBJECTIF] ASSERTIONS APPROXIMATIVES
# ----------------------------------------------------------------------------

"""
NOMBRES FLOTTANTS
─────────────────

[ATTENTION] Ne JAMAIS comparer floats avec ==
"""

def test_floats():
    # [X] MAUVAIS
    # assert 0.1 + 0.2 == 0.3  # Échoue ! (0.30000000000000004)
    
    # [OK] BON : pytest.approx()
    import pytest
    assert 0.1 + 0.2 == pytest.approx(0.3)
    
    # Avec tolérance personnalisée
    assert 10.0 == pytest.approx(10.1, abs=0.2)
    
    # Tolérance relative
    assert 1000 == pytest.approx(1001, rel=0.01)  # 1% tolérance

"""
LISTES DE FLOATS
────────────────
"""

def test_liste_floats():
    import pytest
    
    resultats = [0.1 + 0.1, 0.2 + 0.1, 0.3 + 0.1]
    attendus = [0.2, 0.3, 0.4]
    
    assert resultats == pytest.approx(attendus)

# ----------------------------------------------------------------------------
# [ALERTE] TESTER LES EXCEPTIONS
# ----------------------------------------------------------------------------

"""
VÉRIFIER QU'EXCEPTION EST LEVÉE
────────────────────────────────
"""

import pytest

def test_exception_levee():
    # Fonction qui lève exception
    def diviser(a, b):
        if b == 0:
            raise ValueError("Division par zéro")
        return a / b
    
    # Vérifier exception
    with pytest.raises(ValueError):
        diviser(10, 0)

"""
VÉRIFIER MESSAGE D'ERREUR
──────────────────────────
"""

def test_message_exception():
    def valider_age(age):
        if age < 0:
            raise ValueError("L'âge ne peut pas être négatif")
    
    # Capturer exception
    with pytest.raises(ValueError) as exc_info:
        valider_age(-5)
    
    # Vérifier message exact
    assert str(exc_info.value) == "L'âge ne peut pas être négatif"
    
    # Ou contient
    assert "négatif" in str(exc_info.value)

"""
MATCH PATTERN (regex)
─────────────────────
"""

def test_exception_pattern():
    def process_data(data):
        if not isinstance(data, dict):
            raise TypeError(f"Expected dict, got {type(data).__name__}")
    
    # Match pattern
    with pytest.raises(TypeError, match=r"Expected dict, got \w+"):
        process_data("invalid")

"""
PLUSIEURS EXCEPTIONS POSSIBLES
───────────────────────────────
"""

def test_plusieurs_exceptions():
    def operation_risquee(value):
        if value < 0:
            raise ValueError("Négatif")
        if value == 0:
            raise ZeroDivisionError("Zéro")
        return 100 / value
    
    # TypeError OU ValueError
    with pytest.raises((ValueError, ZeroDivisionError)):
        operation_risquee(0)

# ----------------------------------------------------------------------------
# [ATTENTION] WARNINGS ET DEPRECATIONS
# ----------------------------------------------------------------------------

"""
TESTER WARNINGS
───────────────
"""

def test_warnings():
    import warnings
    
    def fonction_deprecated():
        warnings.warn("Cette fonction est obsolète", DeprecationWarning)
        return 42
    
    # Vérifier warning
    with pytest.warns(DeprecationWarning):
        fonction_deprecated()
    
    # Vérifier message
    with pytest.warns(DeprecationWarning, match="obsolète"):
        fonction_deprecated()

# ----------------------------------------------------------------------------
# [DESIGN] ASSERTIONS CUSTOM
# ----------------------------------------------------------------------------

"""
CRÉER ASSERTIONS PERSONNALISÉES
────────────────────────────────
"""

def est_pair(nombre):
    """Helper d'assertion custom"""
    assert nombre % 2 == 0, f"{nombre} n'est pas pair"

def test_avec_assertion_custom():
    est_pair(4)  # [OK] Passe
    # est_pair(5)  # [X] Échoue avec message clair

"""
MATCHERS CUSTOM (style hamcrest)
─────────────────────────────────
"""

def est_email_valide(email):
    """Matcher custom pour email"""
    import re
    pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
    assert re.match(pattern, email), f"{email} n'est pas un email valide"

def test_email():
    est_email_valide("user@example.com")
    # est_email_valide("invalid")  # Échoue

"""
ASSERTION AVEC CONTEXTE
───────────────────────
"""

def assert_utilisateur_valide(user):
    """Validation complexe d'utilisateur"""
    assert user is not None, "Utilisateur ne peut pas être None"
    assert "email" in user, "Email manquant"
    assert "name" in user, "Nom manquant"
    assert len(user["name"]) > 0, "Nom vide"
    assert "@" in user["email"], "Email invalide"

def test_utilisateur():
    user = {"email": "alice@example.com", "name": "Alice"}
    assert_utilisateur_valide(user)

# ----------------------------------------------------------------------------
# [GRAPHIQUE] ASSERTIONS SUR OBJETS
# ----------------------------------------------------------------------------

"""
ATTRIBUTS D'OBJETS
──────────────────
"""

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
        self.active = True

def test_attributs_objet():
    user = User("Alice", "alice@example.com")
    
    # Attributs existent
    assert hasattr(user, "name")
    assert hasattr(user, "email")
    
    # Valeurs attributs
    assert user.name == "Alice"
    assert user.email == "alice@example.com"
    assert user.active is True
    
    # Type
    assert isinstance(user, User)

"""
MÉTHODES D'OBJETS
─────────────────
"""

def test_methodes_objet():
    class Calculatrice:
        def additionner(self, a, b):
            return a + b
    
    calc = Calculatrice()
    
    # Méthode existe
    assert hasattr(calc, "additionner")
    assert callable(calc.additionner)
    
    # Appel méthode
    resultat = calc.additionner(2, 3)
    assert resultat == 5

"""
ÉGALITÉ D'OBJETS
────────────────
"""

def test_egalite_objets():
    user1 = User("Alice", "alice@example.com")
    user2 = User("Alice", "alice@example.com")
    user3 = user1
    
    # Identité (même objet)
    assert user1 is user3
    assert user1 is not user2
    
    # Égalité (nécessite __eq__)
    # Sans __eq__, user1 != user2 même avec mêmes attributs

# ----------------------------------------------------------------------------
# [RECHERCHE] ASSERTIONS AVANCÉES pytest
# ----------------------------------------------------------------------------

"""
SKIP ET XFAIL
─────────────
"""

@pytest.mark.skip(reason="Feature pas encore implémentée")
def test_skip():
    assert False  # Ne sera pas exécuté

@pytest.mark.skipif(sys.version_info < (3, 10), reason="Python 3.10+ requis")
def test_skip_conditionnel():
    # Exécuté seulement si Python >= 3.10
    assert True

@pytest.mark.xfail(reason="Bug connu #123")
def test_expected_to_fail():
    assert False  # Marqué comme "échec attendu"

"""
PARAMETRIZE (Tests paramétrés)
───────────────────────────────
"""

@pytest.mark.parametrize("a,b,attendu", [
    (2, 3, 5),
    (10, 15, 25),
    (-5, -3, -8),
    (0, 0, 0),
])
def test_addition_parametree(a, b, attendu):
    """Un test exécuté 4 fois avec différentes valeurs"""
    calc = Calculatrice()
    assert calc.additionner(a, b) == attendu

"""
FIXTURES (Setup/Teardown)
──────────────────────────
"""

@pytest.fixture
def calculatrice():
    """Fixture qui crée une calculatrice"""
    calc = Calculatrice()
    return calc

def test_avec_fixture(calculatrice):
    """calculatrice est injectée automatiquement"""
    assert calculatrice.additionner(2, 3) == 5

@pytest.fixture
def base_de_donnees():
    """Fixture avec setup et teardown"""
    # Setup
    db = Database()
    db.connect()
    
    yield db  # Fournir au test
    
    # Teardown (après le test)
    db.disconnect()

def test_avec_db(base_de_donnees):
    base_de_donnees.insert_user("Alice")
    assert base_de_donnees.count_users() == 1

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Assertions de base
   - Égalité, comparaisons, booléens
   - Messages d'erreur pytest

[OK] Assertions collections
   - Listes, dicts, sets
   - Contenance, longueur

[OK] Nombres flottants
   - pytest.approx()
   - Tolérances

[OK] Exceptions
   - pytest.raises()
   - Vérifier messages
   - Match patterns

[OK] Assertions custom
   - Helpers personnalisés
   - Matchers métier

[OK] Fonctionnalités avancées
   - Parametrize
   - Fixtures
   - Skip/xfail


[CLE] POINTS CLÉS

1. Assert natif Python + pytest
   Syntaxe simple, messages riches

2. Un assert = Une vérification
   Tests clairs et précis

3. pytest.approx() pour floats
   Jamais == direct

4. pytest.raises() pour exceptions
   Tester aussi les erreurs

5. Assertions custom = Clarté
   Abstraire validations complexes


[RAPIDE] PROCHAINE ÉTAPE

Chapitre 7 : Types de Tests
Unitaires, Intégration, E2E, etc.
"""


# ============================================================================
# [GUIDE] CHAPITRE 7 : TYPES DE TESTS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Distinguer types de tests
[OK] Écrire tests unitaires purs
[OK] Créer tests d'intégration
[OK] Comprendre pyramide des tests
[OK] Organiser suite de tests
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] LA PYRAMIDE DES TESTS
# ----------------------------------------------------------------------------

"""
PYRAMIDE DES TESTS (Mike Cohn)

            ╱╲
           ╱E2E╲         <- Peu nombreux, lents
          ╱──────╲
         ╱ Intég. ╲      <- Quantité moyenne
        ╱──────────╲
       ╱  Unitaires ╲    <- Nombreux, rapides
      ╱──────────────╲
     ╱________________╲


CARACTÉRISTIQUES
────────────────

Tests Unitaires (70-80%)
├─ Isolés (pas de dépendances externes)
├─ Rapides (< 100ms)
├─ Focalisés (une fonction/classe)
└─ Nombreux

Tests d'Intégration (15-20%)
├─ Plusieurs composants ensemble
├─ Moyennement rapides (< 1s)
├─ Dépendances réelles (DB, API)
└─ Quantité moyenne

Tests E2E (5-10%)
├─ Système complet
├─ Lents (secondes/minutes)
├─ Interface utilisateur
└─ Peu nombreux


[IDEE] POURQUOI CETTE PYRAMIDE ?

Coût/Bénéfice :
- Tests unitaires : Rapides + Précis = Nombreux
- Tests E2E : Lents + Fragiles = Peu

Feedback :
- Unitaires : Feedback immédiat
- E2E : Feedback lent
"""

# ----------------------------------------------------------------------------
# [TEST] TESTS UNITAIRES
# ----------------------------------------------------------------------------

"""
DÉFINITION
──────────

Test unitaire = Test d'une SEULE unité logique
- Une fonction
- Une méthode
- Une classe

Caractéristiques :
[OK] ISOLÉ (pas de DB, fichiers, réseau)
[OK] RAPIDE (< 100ms)
[OK] DÉTERMINISTE (toujours même résultat)
[OK] INDÉPENDANT (ordre d'exécution n'importe pas)


EXEMPLE PARFAIT
───────────────
"""

# Code de production
def calculer_remise(prix, pourcentage):
    """Calcule le prix après remise"""
    if pourcentage < 0 or pourcentage > 100:
        raise ValueError("Pourcentage invalide")
    
    remise = prix * (pourcentage / 100)
    return prix - remise

# Test unitaire
def test_calculer_remise_20_pourcent():
    # [OK] Isolé : pas de dépendances
    # [OK] Rapide : calcul pur
    # [OK] Déterministe : toujours même résultat
    resultat = calculer_remise(100, 20)
    assert resultat == 80

def test_calculer_remise_pourcentage_invalide():
    # Test des edge cases
    with pytest.raises(ValueError):
        calculer_remise(100, 150)

"""
CONTRE-EXEMPLE (pas unitaire)
──────────────────────────────
"""

def test_sauvegarder_utilisateur():
    # [X] PAS unitaire : utilise DB
    db = Database()
    db.connect()
    
    user_service = UserService(db)
    user_service.save_user("Alice", "alice@example.com")
    
    # Vérifie en DB
    saved = db.get_user("Alice")
    assert saved.email == "alice@example.com"
    
    db.disconnect()

# Ce test est un test d'INTÉGRATION

"""
RENDRE UNITAIRE AVEC MOCKS
───────────────────────────
"""

def test_sauvegarder_utilisateur_unitaire():
    # [OK] Unitaire : mock la DB
    mock_db = Mock()
    
    user_service = UserService(mock_db)
    user_service.save_user("Alice", "alice@example.com")
    
    # Vérifie que DB a été appelée correctement
    mock_db.insert.assert_called_once_with("Alice", "alice@example.com")

"""
QUAND ÉCRIRE TESTS UNITAIRES ?
───────────────────────────────

[OK] Logique métier pure
[OK] Algorithmes
[OK] Calculs
[OK] Validations
[OK] Transformations de données
[OK] Utilitaires

[X] UI
[X] Intégrations systèmes
[X] Configuration
"""

# ----------------------------------------------------------------------------
# [LIEN] TESTS D'INTÉGRATION
# ----------------------------------------------------------------------------

"""
DÉFINITION
──────────

Test d'intégration = Test de plusieurs composants ensemble
- Service + Database
- API + Service
- Multiple classes

Caractéristiques :
[OK] DÉPENDANCES RÉELLES (DB, fichiers)
[OK] Plus LENTS (< 1s idéalement)
[OK] Vérifie INTERACTIONS
[OK] Moins nombreux


EXEMPLE
───────
"""

def test_creer_utilisateur_integration():
    """Test d'intégration complet"""
    # Setup : DB de test
    db = TestDatabase()
    db.setup()
    
    # Service réel avec DB réelle
    user_service = UserService(db)
    email_service = EmailService()
    
    # Action : créer utilisateur
    user = user_service.create_user(
        name="Alice",
        email="alice@example.com",
        email_service=email_service
    )
    
    # Vérifications multiples
    # 1. User créé en DB
    saved_user = db.get_user(user.id)
    assert saved_user.name == "Alice"
    
    # 2. Email envoyé
    assert email_service.sent_emails == 1
    
    # 3. User actif
    assert user.is_active
    
    # Teardown
    db.teardown()

"""
TESTS AVEC BASE DE DONNÉES
───────────────────────────
"""

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

@pytest.fixture
def db_session():
    """Fixture pour DB de test"""
    # Créer DB en mémoire
    engine = create_engine('sqlite:///:memory:')
    Base.metadata.create_all(engine)
    
    Session = sessionmaker(bind=engine)
    session = Session()
    
    yield session  # Fournir au test
    
    # Cleanup
    session.close()

def test_repository_integration(db_session):
    """Test du repository avec vraie DB"""
    repo = UserRepository(db_session)
    
    # Créer
    user = User(name="Alice", email="alice@example.com")
    repo.save(user)
    
    # Lire
    found = repo.find_by_email("alice@example.com")
    assert found.name == "Alice"
    
    # Mettre à jour
    found.name = "Alice Smith"
    repo.save(found)
    
    # Vérifier
    updated = repo.find_by_email("alice@example.com")
    assert updated.name == "Alice Smith"

"""
TESTS AVEC API EXTERNE
───────────────────────
"""

def test_appel_api_integration():
    """Test avec vraie API (ou API de test)"""
    # Utiliser API de test/staging
    api = WeatherAPI(base_url="https://api.test.weather.com")
    
    # Appel réel
    weather = api.get_weather("Paris")
    
    # Vérifications
    assert weather.city == "Paris"
    assert weather.temperature is not None
    assert 0 <= weather.temperature <= 50  # Valeurs réalistes

"""
[ATTENTION] BONNES PRATIQUES INTÉGRATION

1. ISOLER ENVIRONNEMENT
   - DB de test séparée
   - API de test/staging
   - Pas production !

2. CLEANUP APRÈS TEST
   - Rollback transactions
   - Supprimer données test
   - Fixtures avec teardown

3. TESTS RÉPLICABLES
   - Données fixtures contrôlées
   - Pas de dépendances externes instables
   - Éviter tests flaky

4. PLUS LENT = MOINS NOMBREUX
   - Tester scénarios critiques
   - Pas tous les cas limites
"""

# ----------------------------------------------------------------------------
# [WEB] TESTS END-TO-END (E2E)
# ----------------------------------------------------------------------------

"""
DÉFINITION
──────────

Test E2E = Test du système COMPLET
- Du point de vue utilisateur
- Interface UI réelle
- Toute la stack

Caractéristiques :
[OK] SYSTÈME COMPLET
[OK] TRÈS LENTS (secondes/minutes)
[OK] VUE UTILISATEUR
[OK] PEU NOMBREUX (5-10% tests)


EXEMPLE (Selenium - Web)
─────────────────────────
"""

from selenium import webdriver
from selenium.webdriver.common.by import By

def test_inscription_utilisateur_e2e():
    """Test E2E complet inscription"""
    # Setup : Navigateur
    driver = webdriver.Chrome()
    
    try:
        # 1. Ouvrir page
        driver.get("http://localhost:8000/register")
        
        # 2. Remplir formulaire
        driver.find_element(By.ID, "name").send_keys("Alice")
        driver.find_element(By.ID, "email").send_keys("alice@example.com")
        driver.find_element(By.ID, "password").send_keys("secret123")
        
        # 3. Soumettre
        driver.find_element(By.ID, "submit").click()
        
        # 4. Vérifier redirection
        assert "dashboard" in driver.current_url
        
        # 5. Vérifier message
        welcome = driver.find_element(By.CLASS_NAME, "welcome-message")
        assert "Bienvenue Alice" in welcome.text
        
    finally:
        driver.quit()

"""
EXEMPLE (Playwright - Modern)
──────────────────────────────
"""

from playwright.sync_api import sync_playwright

def test_login_e2e():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        # Navigation
        page.goto("http://localhost:8000/login")
        
        # Remplir formulaire
        page.fill('input[name="email"]', "alice@example.com")
        page.fill('input[name="password"]', "secret123")
        page.click('button[type="submit"]')
        
        # Attendre navigation
        page.wait_for_url("**/dashboard")
        
        # Vérifications
        assert page.title() == "Dashboard - Mon App"
        assert page.locator(".user-name").inner_text() == "Alice"
        
        browser.close()

"""
EXEMPLE (API E2E)
─────────────────
"""

import requests

def test_workflow_complet_api():
    """Test E2E d'un workflow API complet"""
    base_url = "http://localhost:8000/api"
    
    # 1. Créer utilisateur
    response = requests.post(f"{base_url}/users", json={
        "name": "Alice",
        "email": "alice@example.com"
    })
    assert response.status_code == 201
    user_id = response.json()["id"]
    
    # 2. Créer post
    response = requests.post(f"{base_url}/posts", json={
        "title": "Mon post",
        "content": "Contenu...",
        "user_id": user_id
    })
    assert response.status_code == 201
    post_id = response.json()["id"]
    
    # 3. Ajouter commentaire
    response = requests.post(f"{base_url}/posts/{post_id}/comments", json={
        "text": "Super post!",
        "user_id": user_id
    })
    assert response.status_code == 201
    
    # 4. Récupérer post avec commentaires
    response = requests.get(f"{base_url}/posts/{post_id}?include=comments")
    data = response.json()
    assert len(data["comments"]) == 1
    assert data["comments"][0]["text"] == "Super post!"

"""
[ATTENTION] DÉFIS DES TESTS E2E

1. LENTEUR
   - Secondes/minutes par test
   - Impossible d'en avoir beaucoup

2. FRAGILITÉ
   - Changements UI cassent tests
   - Timeouts aléatoires
   - Tests flaky

3. MAINTENANCE
   - Coûteux à maintenir
   - Beaucoup de code

4. DEBUGGING DIFFICILE
   - Beaucoup de composants
   - Difficile d'isoler problème


[IDEE] STRATÉGIE E2E

[OK] Tester parcours utilisateur critiques
   - Inscription
   - Login
   - Achat
   - Paiement

[X] Ne pas tester tous les cas
   - Laisser ça aux tests unitaires
   - Focus sur happy path + 2-3 erreurs
"""

# ----------------------------------------------------------------------------
# [GRAPHIQUE] AUTRES TYPES DE TESTS
# ----------------------------------------------------------------------------

"""
TESTS DE PERFORMANCE
────────────────────
"""

import time

def test_performance_calcul():
    """Vérifier que fonction est assez rapide"""
    start = time.time()
    
    resultat = fonction_complexe(1000)
    
    duree = time.time() - start
    
    # Doit finir en < 1 seconde
    assert duree < 1.0
    assert resultat is not None

"""
TESTS DE CHARGE
───────────────

Utiliser outils spécialisés :
- Locust (Python)
- JMeter
- k6

Exemple Locust :
"""

from locust import HttpUser, task, between

class UserBehavior(HttpUser):
    wait_time = between(1, 3)
    
    @task
    def get_homepage(self):
        self.client.get("/")
    
    @task(3)  # 3x plus fréquent
    def get_product(self):
        self.client.get("/products/123")

"""
TESTS DE SÉCURITÉ
─────────────────

- Injection SQL
- XSS
- CSRF
- Authentification

Exemple :
"""

def test_sql_injection_prevention():
    """Vérifier protection contre SQL injection"""
    # Tentative d'injection
    malicious_input = "'; DROP TABLE users; --"
    
    # Ne doit PAS planter ou supprimer table
    result = search_users(malicious_input)
    
    # Doit retourner vide (pas de résultat)
    assert result == []
    
    # Table existe toujours
    assert User.query.count() > 0

"""
TESTS DE RÉGRESSION
───────────────────

Tests pour bugs corrigés :
"""

def test_bug_123_division_par_zero():
    """
    Régression test pour Bug #123
    Division par zéro plantait l'app
    """
    calc = Calculatrice()
    
    # Ne doit plus planter
    with pytest.raises(ValueError):
        calc.diviser(10, 0)
    
    # Message clair
    try:
        calc.diviser(10, 0)
    except ValueError as e:
        assert "Division par zéro" in str(e)

"""
SMOKE TESTS
───────────

Tests rapides pour vérifier que système fonctionne :
"""

def test_smoke_application_demarre():
    """Smoke test : app démarre sans erreur"""
    response = requests.get("http://localhost:8000/health")
    assert response.status_code == 200
    assert response.json()["status"] == "ok"

def test_smoke_database_accessible():
    """Smoke test : DB accessible"""
    db = Database()
    assert db.ping() is True

"""
TESTS DE CONTRAT (API)
──────────────────────

Vérifier que API respecte contrat :
"""

def test_contrat_api_users():
    """Vérifier schéma réponse API"""
    response = requests.get("http://localhost:8000/api/users/1")
    
    data = response.json()
    
    # Schéma attendu
    assert "id" in data
    assert "name" in data
    assert "email" in data
    assert isinstance(data["id"], int)
    assert isinstance(data["name"], str)
    assert "@" in data["email"]

# ----------------------------------------------------------------------------
# [DOSSIER] ORGANISER LES TESTS
# ----------------------------------------------------------------------------

"""
STRUCTURE RECOMMANDÉE
─────────────────────
"""

tests/
├── unit/                   # Tests unitaires
│   ├── test_calculatrice.py
│   ├── test_panier.py
│   └── test_validations.py
│
├── integration/            # Tests d'intégration
│   ├── test_user_service.py
│   ├── test_database.py
│   └── test_api_calls.py
│
├── e2e/                    # Tests E2E
│   ├── test_user_journey.py
│   └── test_checkout.py
│
├── performance/            # Tests de perf
│   └── test_load.py
│
├── conftest.py            # Fixtures partagées
└── pytest.ini             # Configuration

"""
MARKERS pytest.ini
──────────────────
"""

# pytest.ini
"""
[pytest]
markers =
    unit: Tests unitaires rapides
    integration: Tests d'intégration
    e2e: Tests end-to-end
    slow: Tests lents
    smoke: Smoke tests
"""

"""
UTILISATION MARKERS
───────────────────
"""

@pytest.mark.unit
def test_addition():
    assert 2 + 2 == 4

@pytest.mark.integration
@pytest.mark.slow
def test_database_query():
    # Test d'intégration lent
    pass

# Lancer seulement tests unitaires
# pytest -m unit

# Exclure tests lents
# pytest -m "not slow"

# Combiner
# pytest -m "unit or integration"

"""
NOMMAGE FICHIERS
────────────────

Conventions :
test_<module>.py        -> Tests du module
test_integration_<feature>.py -> Tests intégration
test_e2e_<workflow>.py  -> Tests E2E
"""

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Pyramide des tests
   - Unitaires (70-80%)
   - Intégration (15-20%)
   - E2E (5-10%)

[OK] Tests unitaires
   - Isolés, rapides, nombreux
   - Logique métier pure
   - Mocks pour dépendances

[OK] Tests d'intégration
   - Plusieurs composants
   - DB, API réelles
   - Plus lents mais essentiels

[OK] Tests E2E
   - Système complet
   - Vue utilisateur
   - Peu nombreux, critiques

[OK] Autres types
   - Performance, sécurité
   - Régression, smoke
   - Contrats API

[OK] Organisation
   - Structure claire
   - Markers pytest
   - Exécution sélective


[CLE] POINTS CLÉS

1. Pyramide = Guide
   Majorité unitaires, peu E2E

2. Chaque type a son rôle
   Ne pas tout tester en E2E

3. Unitaires = Rapidité TDD
   Feedback < 1 seconde

4. Intégration = Confiance
   Composants fonctionnent ensemble

5. E2E = Validation finale
   Parcours utilisateur OK


[THOUGHT_BALLOON] CITATION

"Write tests. Not too many. Mostly integration."
- Kent C. Dodds (variation de Michael Pollan)


═══════════════════════════════════════════════════════════════
FÉLICITATIONS ! [BRAVO]

Vous avez complété la PARTIE 1 : FONDAMENTAUX PRATIQUES

Vous savez maintenant :
[OK] Setup environnement TDD
[OK] Écrire premier test TDD
[OK] Utiliser assertions efficacement
[OK] Distinguer types de tests
[OK] Organiser suite de tests

Vous êtes prêt pour la PARTIE 2 : PRATIQUE AVANCÉE !
═══════════════════════════════════════════════════════════════
"""

# ============================================================================
# FIN DE LA PARTIE 1 : FONDAMENTAUX PRATIQUES
# SUITE DANS tdd_partie2.md
# ============================================================================

# ============================================================================
# [LIVRE] TDD - PARTIE 2 : PRATIQUE AVANCÉE
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 8 : Mocks et Stubs
# - Chapitre 9 : Test de Code Legacy
# - Chapitre 10 : Design Patterns TDD
# - Chapitre 11 : TDD avec Bases de Données
# - Chapitre 12 : TDD avec APIs
#
# [TEMPS] TEMPS : ~8-10 heures
# [DOCS] PRÉREQUIS : Parties 0 et 1 complétées
# ============================================================================

"""
[OBJECTIF] OBJECTIFS DE LA PARTIE 2

À la fin de cette partie, vous saurez :
[OK] Utiliser mocks et stubs efficacement
[OK] Ajouter tests au code legacy
[OK] Appliquer design patterns TDD
[OK] Tester avec bases de données
[OK] Tester APIs et services externes
[OK] Gérer dépendances complexes
"""

# ============================================================================
# [GUIDE] CHAPITRE 8 : MOCKS ET STUBS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Différencier mock, stub, fake, spy
[OK] Utiliser unittest.mock (Python)
[OK] Isoler tests avec mocks
[OK] Éviter over-mocking
[OK] Tester interactions
"""

# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI LES MOCKS ?
# ----------------------------------------------------------------------------

"""
PROBLÈME : DÉPENDANCES EXTERNES
────────────────────────────────

Code réel avec dépendances :
"""

class UserService:
    def __init__(self, database, email_service, logger):
        self.db = database
        self.email = email_service
        self.logger = logger
    
    def create_user(self, name, email):
        # 1. Sauvegarder en DB
        user = self.db.insert_user(name, email)
        
        # 2. Envoyer email de bienvenue
        self.email.send_welcome(email)
        
        # 3. Logger l'action
        self.logger.info(f"User created: {name}")
        
        return user

"""
[X] PROBLÈMES POUR TESTER

1. BASE DE DONNÉES
   - Besoin d'une vraie DB
   - Setup/teardown complexe
   - Tests lents

2. EMAIL
   - Envoie vraiment des emails !
   - Ou erreur si config manquante
   - Difficile à vérifier

3. LOGGER
   - Écrit dans des fichiers
   - Pollue les logs

-> Test devient un test d'INTÉGRATION
-> Lent, fragile, complexe


[OK] SOLUTION : MOCKS

Mock = Objet simulé qui remplace une dépendance réelle
"""

def test_create_user_avec_mocks():
    # Créer des mocks
    mock_db = Mock()
    mock_email = Mock()
    mock_logger = Mock()
    
    # Service avec mocks
    service = UserService(mock_db, mock_email, mock_logger)
    
    # Action
    user = service.create_user("Alice", "alice@example.com")
    
    # Vérifications
    mock_db.insert_user.assert_called_once_with("Alice", "alice@example.com")
    mock_email.send_welcome.assert_called_once_with("alice@example.com")
    mock_logger.info.assert_called()

"""
[OK] AVANTAGES

- Test UNITAIRE (isolé)
- Rapide (< 10ms)
- Pas de dépendances
- Vérifie COMPORTEMENT
"""

# ----------------------------------------------------------------------------
# [DOCS] VOCABULAIRE : Mock, Stub, Fake, Spy
# ----------------------------------------------------------------------------

"""
TEST DOUBLES (Doublures de test)

Terme générique pour objets de remplacement dans tests


1. STUB (Bouchon)
─────────────────

Définition : Fournit des réponses pré-programmées

Utilisation : Quand on a besoin de données spécifiques
"""

class StubDatabase:
    """Stub qui retourne toujours les mêmes données"""
    
    def get_user(self, user_id):
        # Toujours même résultat
        return User(id=1, name="Alice", email="alice@example.com")
    
    def get_users(self):
        return [
            User(id=1, name="Alice"),
            User(id=2, name="Bob")
        ]

def test_avec_stub():
    stub_db = StubDatabase()
    service = UserService(stub_db)
    
    # Utilise les données du stub
    user = service.get_user_by_id(1)
    assert user.name == "Alice"

"""
[IDEE] STUB = Données fixes pour le test


2. MOCK (Simulacre)
───────────────────

Définition : Vérifie les interactions (appels de méthodes)

Utilisation : Quand on veut vérifier COMMENT le code utilise dépendances
"""

from unittest.mock import Mock

def test_avec_mock():
    # Mock vérifie les appels
    mock_email = Mock()
    
    service = NotificationService(mock_email)
    service.notify_user("alice@example.com", "Hello")
    
    # Vérifier que méthode a été appelée
    mock_email.send.assert_called_once_with(
        to="alice@example.com",
        message="Hello"
    )

"""
[IDEE] MOCK = Vérification d'interactions


3. FAKE (Simulateur)
────────────────────

Définition : Implémentation simplifiée mais fonctionnelle

Utilisation : Alternative légère à vrai système
"""

class FakeDatabase:
    """Fake DB en mémoire"""
    
    def __init__(self):
        self.users = {}
        self.next_id = 1
    
    def insert_user(self, name, email):
        user = User(id=self.next_id, name=name, email=email)
        self.users[self.next_id] = user
        self.next_id += 1
        return user
    
    def get_user(self, user_id):
        return self.users.get(user_id)

def test_avec_fake():
    fake_db = FakeDatabase()
    service = UserService(fake_db)
    
    # Fonctionne comme vraie DB mais en mémoire
    user = service.create_user("Alice", "alice@example.com")
    found = service.get_user(user.id)
    
    assert found.name == "Alice"

"""
[IDEE] FAKE = Implémentation simplifiée qui marche


4. SPY (Espion)
───────────────

Définition : Enregistre les appels mais exécute aussi le vrai code

Utilisation : Observer comportement sans le bloquer
"""

from unittest.mock import MagicMock

def test_avec_spy():
    # Spy enregistre ET exécute
    real_service = EmailService()
    spy = MagicMock(wraps=real_service)
    
    spy.send_email("alice@example.com", "Hello")
    
    # Vérifier appel
    spy.send_email.assert_called_once()
    
    # Vraie méthode a été exécutée aussi

"""
[IDEE] SPY = Observation + Exécution réelle


RÉSUMÉ VISUEL
─────────────

                    Retourne        Vérifie      Exécute
                    données ?       appels ?     vrai code ?
    ──────────────────────────────────────────────────────
    Stub            [OK] (fixe)       [X]           [X]
    Mock            [X]              [OK]           [X]
    Fake            [OK] (simple)     [X]           [ATTENTION] (simulé)
    Spy             [OK]              [OK]           [OK]


[IDEE] EN PRATIQUE

Le terme "mock" est souvent utilisé pour tout
Mais comprendre nuances aide à mieux tester
"""

# ----------------------------------------------------------------------------
# [PYTHON] unittest.mock (Python)
# ----------------------------------------------------------------------------

"""
BIBLIOTHÈQUE STANDARD PYTHON

unittest.mock = Puissant système de mocking


CRÉER UN MOCK
─────────────
"""

from unittest.mock import Mock, MagicMock

# Mock simple
mock = Mock()

# Appeler n'importe quelle méthode (retourne nouveau Mock)
result = mock.some_method()
result = mock.another_method(1, 2, 3)

# MagicMock : Support méthodes magiques (__str__, __len__, etc.)
magic_mock = MagicMock()
print(len(magic_mock))  # Fonctionne
print(str(magic_mock))  # Fonctionne

"""
CONFIGURER RETOUR
─────────────────
"""

# return_value : Valeur de retour
mock = Mock()
mock.get_user.return_value = User(id=1, name="Alice")

user = mock.get_user()
print(user.name)  # "Alice"

# Différentes valeurs selon appels
mock.get_value.side_effect = [1, 2, 3]
print(mock.get_value())  # 1
print(mock.get_value())  # 2
print(mock.get_value())  # 3

"""
LEVER EXCEPTION
───────────────
"""

mock = Mock()
mock.dangerous_operation.side_effect = ValueError("Oops!")

try:
    mock.dangerous_operation()
except ValueError as e:
    print(e)  # "Oops!"

"""
VÉRIFIER APPELS
───────────────
"""

mock = Mock()
mock.send_email("alice@example.com", "Hello")

# A été appelé ?
mock.send_email.assert_called()

# Appelé une fois ?
mock.send_email.assert_called_once()

# Avec arguments spécifiques ?
mock.send_email.assert_called_with("alice@example.com", "Hello")

# Appelé une fois avec arguments ?
mock.send_email.assert_called_once_with("alice@example.com", "Hello")

# Nombre d'appels
assert mock.send_email.call_count == 1

# Jamais appelé
mock.other_method.assert_not_called()

"""
ARGUMENTS D'APPEL
─────────────────
"""

mock = Mock()
mock.process(1, 2, key="value")

# Accéder aux arguments
args, kwargs = mock.process.call_args
print(args)    # (1, 2)
print(kwargs)  # {'key': 'value'}

# Tous les appels
mock.process(3, 4)
print(mock.process.call_args_list)
# [call(1, 2, key='value'), call(3, 4)]

"""
ANY : Matcher flexible
──────────────────────
"""

from unittest.mock import ANY

mock = Mock()
mock.log(timestamp=1234567890, message="Info")

# Peu importe timestamp
mock.log.assert_called_with(timestamp=ANY, message="Info")

"""
SPEC : Limiter mock à classe réelle
────────────────────────────────────
"""

class RealService:
    def real_method(self, x):
        pass

# Mock avec spec = Ne peut appeler que méthodes existantes
mock = Mock(spec=RealService)

mock.real_method(1)  # [OK] OK
# mock.fake_method()  # [X] AttributeError

"""
[IDEE] spec = Sécurité contre typos
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] PATCH : Remplacer temporairement
# ----------------------------------------------------------------------------

"""
PATCH = Remplacer objet pendant durée du test


EXEMPLE SANS PATCH (difficile)
───────────────────────────────
"""

# Code de production
import requests

def get_weather(city):
    response = requests.get(f"https://api.weather.com/{city}")
    return response.json()

# Test difficile : appel réel à API
def test_get_weather():
    # Comment mocker requests.get ?
    pass

"""
AVEC PATCH (facile)
───────────────────
"""

from unittest.mock import patch

@patch('requests.get')
def test_get_weather(mock_get):
    # Configurer mock
    mock_get.return_value.json.return_value = {
        "city": "Paris",
        "temp": 20
    }
    
    # Tester fonction
    weather = get_weather("Paris")
    
    # Vérifications
    assert weather["temp"] == 20
    mock_get.assert_called_once_with("https://api.weather.com/Paris")

"""
[IDEE] COMMENT ÇA MARCHE ?

@patch('requests.get') remplace requests.get par un mock
Seulement pendant l'exécution du test
Restauré après


PATCH AVEC CONTEXT MANAGER
───────────────────────────
"""

def test_get_weather():
    with patch('requests.get') as mock_get:
        mock_get.return_value.json.return_value = {"temp": 20}
        
        weather = get_weather("Paris")
        assert weather["temp"] == 20

"""
PATCH MULTIPLE
──────────────
"""

@patch('module.EmailService')
@patch('module.Database')
def test_avec_multiples_patches(mock_db, mock_email):
    # Ordre inverse ! (dernier décorateur = premier argument)
    service = UserService(mock_db, mock_email)
    # ...

# Ou avec context manager
def test_multiples():
    with patch('module.Database') as mock_db, \
         patch('module.EmailService') as mock_email:
        # ...
        pass

"""
PATCH.OBJECT
────────────

Patcher attribut d'un objet spécifique
"""

class MyService:
    def __init__(self):
        self.db = Database()
    
    def get_data(self):
        return self.db.query("SELECT * FROM data")

def test_service():
    service = MyService()
    
    with patch.object(service, 'db') as mock_db:
        mock_db.query.return_value = [1, 2, 3]
        
        data = service.get_data()
        assert data == [1, 2, 3]

"""
PATCH.DICT
──────────

Patcher dictionnaire (ex: os.environ)
"""

import os

def test_environment():
    with patch.dict(os.environ, {'API_KEY': 'test-key'}):
        # os.environ['API_KEY'] == 'test-key' seulement ici
        assert os.environ['API_KEY'] == 'test-key'
    
    # Restauré après

# ----------------------------------------------------------------------------
# [OBJECTIF] EXEMPLES PRATIQUES COMPLETS
# ----------------------------------------------------------------------------

"""
EXEMPLE 1 : SERVICE AVEC DATABASE
──────────────────────────────────
"""

# Code de production
class UserRepository:
    def __init__(self, database):
        self.db = database
    
    def create_user(self, name, email):
        if self.email_exists(email):
            raise ValueError("Email already exists")
        
        user_id = self.db.insert({
            'name': name,
            'email': email,
            'created_at': datetime.now()
        })
        
        return self.db.get(user_id)
    
    def email_exists(self, email):
        return self.db.query({'email': email}) is not None

# Tests avec mocks
from unittest.mock import Mock
import pytest
from datetime import datetime

def test_create_user_success():
    # Setup mock
    mock_db = Mock()
    mock_db.query.return_value = None  # Email n'existe pas
    mock_db.insert.return_value = 1
    mock_db.get.return_value = {'id': 1, 'name': 'Alice'}
    
    # Test
    repo = UserRepository(mock_db)
    user = repo.create_user("Alice", "alice@example.com")
    
    # Vérifications
    assert user['name'] == 'Alice'
    mock_db.insert.assert_called_once()
    
    # Vérifier que timestamp a été passé
    call_args = mock_db.insert.call_args[0][0]
    assert 'created_at' in call_args
    assert isinstance(call_args['created_at'], datetime)

def test_create_user_email_exists():
    # Setup mock : email existe déjà
    mock_db = Mock()
    mock_db.query.return_value = {'id': 1}  # Retourne user existant
    
    repo = UserRepository(mock_db)
    
    # Vérifier exception
    with pytest.raises(ValueError, match="Email already exists"):
        repo.create_user("Bob", "alice@example.com")
    
    # insert ne doit PAS avoir été appelé
    mock_db.insert.assert_not_called()

"""
EXEMPLE 2 : SERVICE AVEC API EXTERNE
─────────────────────────────────────
"""

# Code de production
import requests

class WeatherService:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://api.weather.com"
    
    def get_temperature(self, city):
        response = requests.get(
            f"{self.base_url}/weather",
            params={'city': city, 'api_key': self.api_key}
        )
        
        if response.status_code != 200:
            raise Exception(f"API Error: {response.status_code}")
        
        data = response.json()
        return data['temperature']

# Tests avec patch
from unittest.mock import patch, Mock

@patch('requests.get')
def test_get_temperature_success(mock_get):
    # Configurer réponse
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {'temperature': 20}
    mock_get.return_value = mock_response
    
    # Test
    service = WeatherService(api_key="test-key")
    temp = service.get_temperature("Paris")
    
    # Vérifications
    assert temp == 20
    mock_get.assert_called_once_with(
        "https://api.weather.com/weather",
        params={'city': 'Paris', 'api_key': 'test-key'}
    )

@patch('requests.get')
def test_get_temperature_api_error(mock_get):
    # Simuler erreur API
    mock_response = Mock()
    mock_response.status_code = 500
    mock_get.return_value = mock_response
    
    service = WeatherService(api_key="test-key")
    
    with pytest.raises(Exception, match="API Error: 500"):
        service.get_temperature("Paris")

"""
EXEMPLE 3 : MOCK AVEC COMPORTEMENT COMPLEXE
────────────────────────────────────────────
"""

# Code de production
class OrderProcessor:
    def __init__(self, inventory, payment_gateway):
        self.inventory = inventory
        self.payment = payment_gateway
    
    def process_order(self, items, payment_info):
        # 1. Vérifier stock
        for item in items:
            if not self.inventory.check_stock(item.id, item.quantity):
                raise ValueError(f"Insufficient stock for {item.name}")
        
        # 2. Calculer total
        total = sum(item.price * item.quantity for item in items)
        
        # 3. Traiter paiement
        transaction_id = self.payment.charge(payment_info, total)
        
        # 4. Réduire stock
        for item in items:
            self.inventory.reduce_stock(item.id, item.quantity)
        
        return {
            'transaction_id': transaction_id,
            'total': total,
            'status': 'completed'
        }

# Tests
from unittest.mock import Mock
import pytest

class Item:
    def __init__(self, id, name, price, quantity):
        self.id = id
        self.name = name
        self.price = price
        self.quantity = quantity

def test_process_order_success():
    # Setup mocks
    mock_inventory = Mock()
    mock_inventory.check_stock.return_value = True  # Stock OK
    
    mock_payment = Mock()
    mock_payment.charge.return_value = "TXN-12345"
    
    # Test
    processor = OrderProcessor(mock_inventory, mock_payment)
    
    items = [
        Item(1, "Book", 10, 2),
        Item(2, "Pen", 1, 5)
    ]
    
    result = processor.process_order(items, {'card': '1234'})
    
    # Vérifications
    assert result['transaction_id'] == "TXN-12345"
    assert result['total'] == 25  # (10*2) + (1*5)
    assert result['status'] == 'completed'
    
    # Vérifier appels
    assert mock_inventory.check_stock.call_count == 2
    mock_payment.charge.assert_called_once_with({'card': '1234'}, 25)
    assert mock_inventory.reduce_stock.call_count == 2

def test_process_order_insufficient_stock():
    mock_inventory = Mock()
    # Premier item OK, deuxième pas de stock
    mock_inventory.check_stock.side_effect = [True, False]
    
    mock_payment = Mock()
    
    processor = OrderProcessor(mock_inventory, mock_payment)
    
    items = [
        Item(1, "Book", 10, 2),
        Item(2, "Pen", 1, 5)
    ]
    
    # Doit lever exception
    with pytest.raises(ValueError, match="Insufficient stock"):
        processor.process_order(items, {'card': '1234'})
    
    # Paiement ne doit PAS avoir été tenté
    mock_payment.charge.assert_not_called()
    
    # Stock ne doit PAS avoir été réduit
    mock_inventory.reduce_stock.assert_not_called()

# ----------------------------------------------------------------------------
# [ATTENTION] PIÈGES ET ANTI-PATTERNS
# ----------------------------------------------------------------------------

"""
PIÈGE 1 : OVER-MOCKING
──────────────────────

[X] Trop de mocks = Test fragile et couplé à l'implémentation
"""

# [X] MAUVAIS : Mock tout, même les détails internes
def test_over_mocking():
    mock_validator = Mock()
    mock_formatter = Mock()
    mock_logger = Mock()
    mock_cache = Mock()
    mock_metrics = Mock()
    # ... 10 mocks plus tard
    
    # Test devient incompréhensible
    # Tout changement casse le test

# [OK] BON : Mock seulement dépendances externes essentielles
def test_minimal_mocking():
    mock_db = Mock()  # Externe : DB
    mock_api = Mock()  # Externe : API
    
    # Reste est réel
    service = UserService(mock_db, mock_api)

"""
[IDEE] RÈGLE : Ne mocker que ce qui est VRAIMENT externe
- DB, API, système de fichiers, réseau
- PAS les classes métier internes


PIÈGE 2 : TESTER L'IMPLÉMENTATION AU LIEU DU COMPORTEMENT
──────────────────────────────────────────────────────────
"""

# Code de production
def send_notification(user, message):
    formatted = format_message(message)
    log_notification(user, formatted)
    email_service.send(user.email, formatted)

# [X] MAUVAIS : Vérifier détails internes
def test_bad():
    with patch('module.format_message') as mock_format, \
         patch('module.log_notification') as mock_log, \
         patch('module.email_service.send') as mock_send:
        
        send_notification(user, "Hello")
        
        # Test trop couplé à implémentation
        mock_format.assert_called_once_with("Hello")
        mock_log.assert_called_once()
        mock_send.assert_called_once()

# [OK] BON : Tester comportement observable
def test_good():
    with patch('module.email_service') as mock_email:
        send_notification(user, "Hello")
        
        # Vérifie comportement visible : email envoyé
        mock_email.send.assert_called_once()
        assert user.email in mock_email.send.call_args[0]

"""
[IDEE] RÈGLE : Mock interfaces publiques, pas détails internes


PIÈGE 3 : MOCK QUI MENT
────────────────────────
"""

# [X] MAUVAIS : Mock ne ressemble pas à vrai objet
def test_lying_mock():
    mock_db = Mock()
    # Mock retourne string mais vraie DB retourne dict !
    mock_db.get_user.return_value = "Alice"
    
    # Test passe mais échouera en prod
    service = UserService(mock_db)
    user = service.get_user(1)
    print(user.name)  # Crash en prod !

# [OK] BON : Mock ressemble à vrai objet
def test_realistic_mock():
    mock_db = Mock()
    # Retourne structure réaliste
    mock_db.get_user.return_value = User(id=1, name="Alice")
    
    service = UserService(mock_db)
    user = service.get_user(1)
    assert user.name == "Alice"

"""
[IDEE] RÈGLE : Utiliser spec ou créer fixtures réalistes


PIÈGE 4 : PAS DE TESTS D'INTÉGRATION
─────────────────────────────────────

[X] Tout mocker -> Pas de test avec vraies dépendances

[OK] ÉQUILIBRE :
- Tests unitaires avec mocks (nombreux, rapides)
- Tests d'intégration sans mocks (quelques-uns, lents)
"""

def test_integration_real_db():
    """Test avec vraie DB (moins fréquent)"""
    db = TestDatabase()  # Vraie DB de test
    service = UserService(db)
    
    user = service.create_user("Alice", "alice@example.com")
    
    # Vérifie vraiment en DB
    found = db.get_user(user.id)
    assert found.name == "Alice"

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Vocabulaire test doubles
   - Stub : Données fixes
   - Mock : Vérification interactions
   - Fake : Implémentation simple
   - Spy : Observation + exécution

[OK] unittest.mock (Python)
   - Créer mocks
   - Configurer retours
   - Vérifier appels
   - ANY, spec

[OK] Patch
   - Remplacer temporairement
   - @patch décorateur
   - patch.object, patch.dict

[OK] Exemples pratiques
   - Database
   - API externe
   - Comportement complexe

[OK] Pièges à éviter
   - Over-mocking
   - Test implémentation
   - Mock qui ment
   - Manque tests intégration


[CLE] POINTS CLÉS

1. Mock = Isolation
   Tests unitaires rapides

2. Ne pas tout mocker
   Seulement dépendances externes

3. Mock réaliste
   Ressemble au vrai objet

4. Équilibre
   Unitaires (mocks) + Intégration (réel)

5. Tester comportement
   Pas implémentation interne


[THOUGHT_BALLOON] CITATION

"Don't mock what you don't own."
- Steve Freeman & Nat Pryce


[RAPIDE] PROCHAINE ÉTAPE

Chapitre 9 : Test de Code Legacy
Comment ajouter tests au code existant ?
"""

# ============================================================================
# FIN CHAPITRE 8 - SUITE DANS MÊME FICHIER
# ============================================================================

# ============================================================================
# [LIVRE] TDD - PARTIE 2 (SUITE) : CODE LEGACY ET DESIGN PATTERNS
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 9 : TEST DE CODE LEGACY
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Définir code legacy
[OK] Ajouter tests au code existant
[OK] Refactorer en sécurité
[OK] Patterns pour code legacy
[OK] Stratégies progressives
"""

# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QUE LE CODE LEGACY ?
# ----------------------------------------------------------------------------

"""
DÉFINITION (Michael Feathers)
─────────────────────────────

"Code legacy = Code sans tests"

Peu importe :
- L'âge du code
- La qualité du code
- Qui l'a écrit

Sans tests -> Legacy


CARACTÉRISTIQUES CODE LEGACY
─────────────────────────────

[X] Pas de tests (ou très peu)
[X] Couplage fort
[X] Dépendances cachées
[X] Fonctions/classes énormes
[X] Effets de bord partout
[X] Peur de modifier
[X] Documentation obsolète


EXEMPLE TYPIQUE
───────────────
"""

# Code legacy typique (Python)
class UserManager:
    def process_user_registration(self, form_data):
        # Validation
        if not form_data.get('email'):
            return "Error: Email required"
        if not '@' in form_data['email']:
            return "Error: Invalid email"
        
        # Connexion DB directe (couplage fort)
        import mysql.connector
        conn = mysql.connector.connect(
            host="prod-db.example.com",  # [!] DB de prod !
            user="admin",
            password="secret123"
        )
        cursor = conn.cursor()
        
        # Vérifier doublon
        cursor.execute(
            "SELECT * FROM users WHERE email = %s",
            (form_data['email'],)
        )
        if cursor.fetchone():
            conn.close()
            return "Error: Email exists"
        
        # Insérer
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (%s, %s)",
            (form_data['name'], form_data['email'])
        )
        conn.commit()
        
        # Envoyer email (effet de bord)
        import smtplib
        server = smtplib.SMTP('smtp.gmail.com', 587)
        server.starttls()
        server.login("noreply@example.com", "email-password")
        server.sendmail(
            "noreply@example.com",
            form_data['email'],
            "Welcome!"
        )
        server.quit()
        
        # Logger (fichier)
        with open('/var/log/users.log', 'a') as f:
            f.write(f"User registered: {form_data['email']}\n")
        
        conn.close()
        return "Success"

"""
[X] PROBLÈMES POUR TESTER

1. DB prod en dur
2. SMTP en dur
3. Fichier log en dur
4. Tout dans une fonction
5. Effets de bord partout
6. Impossible à tester sans vraie DB/SMTP


[IDEE] OBJECTIF

Ajouter tests SANS tout réécrire d'un coup
Améliorer progressivement
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] STRATÉGIE : CHARACTERIZATION TESTS
# ----------------------------------------------------------------------------

"""
CHARACTERIZATION TESTS (Tests de caractérisation)

Définition : Tests qui DOCUMENTENT comportement actuel
Pas ce qu'on VEUT, mais ce qui EST


OBJECTIF
────────

1. Comprendre comportement actuel
2. Créer filet de sécurité
3. Permettre refactoring
4. Ne PAS corriger bugs encore


EXEMPLE
───────
"""

# Code legacy à tester
def calculate_discount(price, customer_type, quantity):
    """Fonction mystérieuse"""
    if customer_type == "VIP":
        if quantity > 10:
            return price * 0.5
        else:
            return price * 0.7
    elif customer_type == "NORMAL":
        if quantity > 10:
            return price * 0.9
        else:
            return price * 0.95
    else:
        return price

# Characterization tests : observer comportement
def test_characterize_discount():
    """Tests qui documentent comportement existant"""
    
    # VIP avec beaucoup
    assert calculate_discount(100, "VIP", 15) == 50
    
    # VIP avec peu
    assert calculate_discount(100, "VIP", 5) == 70
    
    # NORMAL avec beaucoup
    assert calculate_discount(100, "NORMAL", 15) == 90
    
    # NORMAL avec peu
    assert calculate_discount(100, "NORMAL", 5) == 95
    
    # Autre type
    assert calculate_discount(100, "GUEST", 10) == 100
    
    # Edge case découvert
    assert calculate_discount(100, "vip", 15) == 100  # [!] Case-sensitive !

"""
[IDEE] PROCESSUS

1. Écrire test avec valeur devinée
2. Lancer -> Voir vraie valeur
3. Mettre vraie valeur dans test
4. Répéter pour différents cas

-> Tests documentent comportement réel
-> Même les bugs !


AVANTAGES
─────────

[OK] Filet de sécurité rapide
[OK] Comprend code sans docs
[OK] Révèle bugs cachés
[OK] Permet refactoring
"""

# ----------------------------------------------------------------------------
# [OUTIL] TECHNIQUE : DEPENDENCY INJECTION
# ----------------------------------------------------------------------------

"""
PROBLÈME : DÉPENDANCES EN DUR
──────────────────────────────

Code legacy avec dépendances hardcodées :
"""

class OrderProcessor:
    def process(self, order):
        # DB hardcodée !
        db = MySQLDatabase("localhost", "orders")
        db.save(order)
        
        # Email hardcodé !
        emailer = SMTPEmailer("smtp.gmail.com")
        emailer.send(order.customer_email, "Order confirmed")

"""
[X] Impossible à tester sans vraie DB/SMTP


SOLUTION : INJECTER DÉPENDANCES
────────────────────────────────

Étape 1 : Rendre dépendances paramétrables
"""

class OrderProcessor:
    def __init__(self, database=None, emailer=None):
        # Valeurs par défaut pour compatibilité
        self.db = database or MySQLDatabase("localhost", "orders")
        self.emailer = emailer or SMTPEmailer("smtp.gmail.com")
    
    def process(self, order):
        self.db.save(order)
        self.emailer.send(order.customer_email, "Order confirmed")

"""
[OK] Maintenant testable !
"""

def test_order_processor():
    # Injecter mocks
    mock_db = Mock()
    mock_emailer = Mock()
    
    processor = OrderProcessor(mock_db, mock_emailer)
    order = Order(customer_email="alice@example.com")
    
    processor.process(order)
    
    # Vérifications
    mock_db.save.assert_called_once_with(order)
    mock_emailer.send.assert_called_once()

"""
[IDEE] AVANTAGE

- Code existant fonctionne toujours (valeurs par défaut)
- Testable avec mocks
- Refactoring incrémental


ÉTAPE 2 : SUPPRIMER VALEURS PAR DÉFAUT (optionnel)
───────────────────────────────────────────────────

Une fois tests en place :
"""

class OrderProcessor:
    def __init__(self, database, emailer):
        # Obligatoire maintenant
        self.db = database
        self.emailer = emailer

# Modifier sites d'appel
processor = OrderProcessor(
    MySQLDatabase("localhost", "orders"),
    SMTPEmailer("smtp.gmail.com")
)

# ----------------------------------------------------------------------------
# [OBJECTIF] TECHNIQUE : EXTRACT AND OVERRIDE
# ----------------------------------------------------------------------------

"""
PROBLÈME : DÉPENDANCE DANS MÉTHODE
───────────────────────────────────

Code legacy :
"""

class ReportGenerator:
    def generate(self):
        # Appel direct à service externe
        data = ExternalAPI().fetch_data()
        
        # Traitement
        report = self.process(data)
        
        # Sauvegarde fichier
        with open('/var/reports/report.pdf', 'w') as f:
            f.write(report)
        
        return report

"""
[X] ExternalAPI et fichier en dur


SOLUTION : EXTRACT AND OVERRIDE
────────────────────────────────

Étape 1 : Extraire en méthodes
"""

class ReportGenerator:
    def generate(self):
        data = self.fetch_data()  # <- Extrait
        report = self.process(data)
        self.save_report(report)  # <- Extrait
        return report
    
    def fetch_data(self):
        """Méthode extractée"""
        return ExternalAPI().fetch_data()
    
    def save_report(self, report):
        """Méthode extractée"""
        with open('/var/reports/report.pdf', 'w') as f:
            f.write(report)
    
    def process(self, data):
        # Logique métier pure (déjà testable)
        return f"Report: {data}"

"""
Étape 2 : Override dans tests (subclass)
"""

class TestableReportGenerator(ReportGenerator):
    """Subclass pour tests"""
    
    def __init__(self):
        super().__init__()
        self.saved_report = None
    
    def fetch_data(self):
        # Override avec données test
        return "Test data"
    
    def save_report(self, report):
        # Override sans écrire fichier
        self.saved_report = report

def test_report_generator():
    generator = TestableReportGenerator()
    
    report = generator.generate()
    
    # Vérifications
    assert report == "Report: Test data"
    assert generator.saved_report == report

"""
[IDEE] AVANTAGE

- Pas besoin de toucher code de prod
- Tests passent sans effets de bord
- Refactoring incrémental


VARIANTE : MONKEY PATCHING (Python)
────────────────────────────────────
"""

def test_report_with_monkey_patch():
    generator = ReportGenerator()
    
    # Remplacer méthodes temporairement
    generator.fetch_data = lambda: "Test data"
    generator.save_report = lambda r: None
    
    report = generator.generate()
    assert report == "Report: Test data"

# ----------------------------------------------------------------------------
# [OBJECTIF] TECHNIQUE : SEAM (COUTURE)
# ----------------------------------------------------------------------------

"""
SEAM (Michael Feathers)
───────────────────────

Définition : Point où on peut changer comportement sans modifier code

Types de seams :
1. Object seam (injection)
2. Preprocessing seam (compile-time)
3. Link seam (link-time)


EXEMPLE : OBJECT SEAM
──────────────────────

Code legacy :
"""

class PaymentProcessor:
    def charge(self, amount):
        # Appel direct
        gateway = StripeGateway()
        return gateway.charge(amount)

"""
Créer seam :
"""

class PaymentProcessor:
    def charge(self, amount):
        gateway = self.get_gateway()  # <- Seam
        return gateway.charge(amount)
    
    def get_gateway(self):
        """Seam : peut être overridé"""
        return StripeGateway()

# Test
class TestablePaymentProcessor(PaymentProcessor):
    def get_gateway(self):
        return MockGateway()  # Test double

"""
EXEMPLE : MODULE SEAM (Python)
───────────────────────────────
"""

# module.py
import requests

def fetch_data(url):
    response = requests.get(url)
    return response.json()

# Test avec patch (seam)
from unittest.mock import patch

def test_fetch_data():
    with patch('module.requests.get') as mock_get:
        mock_get.return_value.json.return_value = {"data": "test"}
        
        result = fetch_data("http://example.com")
        assert result["data"] == "test"

# ----------------------------------------------------------------------------
# [LISTE] STRATÉGIE : SPROUT METHOD/CLASS
# ----------------------------------------------------------------------------

"""
SPROUT = Faire pousser nouveau code à côté du legacy


SPROUT METHOD
─────────────

Problème : Fonction énorme, impossible à tester
"""

def process_order(order_data):
    # 500 lignes de code legacy
    # Impossible à tester
    # Couplage partout
    
    # Besoin d'ajouter nouvelle fonctionnalité :
    # Calculer taxes
    pass

"""
Solution : Créer nouvelle méthode (TDD)
"""

def calculate_taxes(order_data):
    """Nouvelle fonction testée en TDD"""
    subtotal = sum(item['price'] for item in order_data['items'])
    tax_rate = 0.20
    return subtotal * tax_rate

# Tests TDD pour nouvelle fonction
def test_calculate_taxes():
    order = {
        'items': [
            {'price': 100},
            {'price': 50}
        ]
    }
    assert calculate_taxes(order) == 30

# Intégrer dans legacy
def process_order(order_data):
    # 500 lignes de legacy...
    
    # Appeler nouvelle fonction testée
    taxes = calculate_taxes(order_data)
    
    # Continuer legacy...
    pass

"""
[IDEE] AVANTAGES

[OK] Nouveau code testé (TDD)
[OK] Legacy pas touché (pas de risque)
[OK] Amélioration progressive


SPROUT CLASS
────────────

Pour fonctionnalité plus complexe :
"""

# Nouvelle classe testée en TDD
class TaxCalculator:
    def __init__(self, tax_rate=0.20):
        self.tax_rate = tax_rate
    
    def calculate(self, items):
        subtotal = sum(item['price'] for item in items)
        return subtotal * self.tax_rate

# Tests
def test_tax_calculator():
    calc = TaxCalculator(tax_rate=0.15)
    items = [{'price': 100}, {'price': 50}]
    assert calc.calculate(items) == 22.5

# Utiliser dans legacy
def process_order(order_data):
    # Legacy...
    
    tax_calc = TaxCalculator()
    taxes = tax_calc.calculate(order_data['items'])
    
    # Legacy...

# ----------------------------------------------------------------------------
# [LISTE] STRATÉGIE : WRAP METHOD/CLASS
# ----------------------------------------------------------------------------

"""
WRAP = Envelopper code legacy


WRAP METHOD
───────────

Problème : Besoin d'ajouter comportement autour du legacy
"""

def save_user(user):
    # Code legacy
    database.insert(user)

"""
Solution : Renommer + créer wrapper
"""

def save_user_to_database(user):
    """Legacy renommé"""
    database.insert(user)

def save_user(user):
    """Nouveau wrapper (testable)"""
    # Avant
    validate_user(user)  # Nouvelle fonction testée
    
    # Legacy
    save_user_to_database(user)
    
    # Après
    send_welcome_email(user)  # Nouvelle fonction testée

"""
WRAP CLASS
──────────

Envelopper classe legacy entière :
"""

class LegacyUserManager:
    """Legacy code (ne pas toucher)"""
    def save(self, user):
        # 1000 lignes de legacy...
        pass

class UserManager:
    """Wrapper moderne (testable)"""
    def __init__(self):
        self.legacy = LegacyUserManager()
        self.validator = UserValidator()  # Nouveau, testé
        self.notifier = EmailNotifier()   # Nouveau, testé
    
    def save(self, user):
        # Avant : nouvelles features testées
        self.validator.validate(user)
        
        # Appel legacy
        self.legacy.save(user)
        
        # Après : nouvelles features testées
        self.notifier.send_welcome(user)

# ----------------------------------------------------------------------------
# [WORLD_MAP] STRATÉGIE GLOBALE : STRANGLER FIG PATTERN
# ----------------------------------------------------------------------------

"""
STRANGLER FIG (Martin Fowler)
──────────────────────────────

Métaphore : Figuier étrangleur qui pousse autour d'un arbre
Progressivement remplace l'arbre original


APPLICATION AU CODE
───────────────────

Phase 1 : Identifier boundaries
"""

# Legacy monolithe
def process_order(order):
    # Validation
    # Inventory check
    # Payment
    # Shipping
    # Notification
    # All in one !
    pass

"""
Phase 2 : Créer nouvelles interfaces (façade)
"""

class OrderProcessor:
    """Nouvelle interface"""
    def __init__(self):
        self.validator = None  # À implémenter
        self.inventory = None
        self.payment = None
    
    def process(self, order):
        # Pour l'instant, délègue au legacy
        return process_order(order)

"""
Phase 3 : Remplacer pièce par pièce (TDD)
"""

class OrderProcessor:
    def __init__(self):
        self.validator = OrderValidator()  # [OK] Nouveau, testé
        self.inventory = None              # [HOURGLASS_WITH_FLOWING_SAND] Legacy encore
        self.payment = None                # [HOURGLASS_WITH_FLOWING_SAND] Legacy encore
    
    def process(self, order):
        # [OK] Validation : nouveau code
        self.validator.validate(order)
        
        # [HOURGLASS_WITH_FLOWING_SAND] Reste : legacy
        # ... appel legacy pour inventory/payment ...

"""
Phase 4 : Continuer jusqu'à remplacement complet
"""

class OrderProcessor:
    def __init__(self):
        self.validator = OrderValidator()     # [OK] Nouveau
        self.inventory = InventoryService()   # [OK] Nouveau
        self.payment = PaymentService()       # [OK] Nouveau
    
    def process(self, order):
        # [OK] Tout nouveau, tout testé
        self.validator.validate(order)
        self.inventory.reserve(order.items)
        self.payment.charge(order.total)

# Legacy complètement remplacé, peut être supprimé !

"""
[IDEE] AVANTAGES

[OK] Progressif (pas de big bang)
[OK] Toujours fonctionnel
[OK] Nouveau code testé
[OK] Risque minimisé
[OK] Business continues


TIMELINE EXEMPLE
────────────────

Semaine 1-2 : Characterization tests
Semaine 3-4 : Créer façade
Semaine 5-8 : Remplacer validation (TDD)
Semaine 9-12 : Remplacer inventory (TDD)
Semaine 13-16 : Remplacer payment (TDD)
Semaine 17 : Supprimer legacy

Total : 4 mois, toujours fonctionnel
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] CAS PRATIQUE COMPLET
# ----------------------------------------------------------------------------

"""
SCÉNARIO : REFACTORER CODE LEGACY
──────────────────────────────────

Code legacy (à améliorer) :
"""

# legacy_code.py
import smtplib
import mysql.connector

class UserRegistration:
    def register(self, email, password, name):
        # Validation
        if not email or not '@' in email:
            return False, "Invalid email"
        
        if len(password) < 8:
            return False, "Password too short"
        
        # DB connection
        conn = mysql.connector.connect(
            host="localhost",
            user="root",
            password="secret",
            database="users_db"
        )
        cursor = conn.cursor()
        
        # Check duplicate
        cursor.execute(
            "SELECT * FROM users WHERE email = %s",
            (email,)
        )
        if cursor.fetchone():
            conn.close()
            return False, "Email exists"
        
        # Insert
        cursor.execute(
            "INSERT INTO users (email, password, name) VALUES (%s, %s, %s)",
            (email, password, name)
        )
        conn.commit()
        user_id = cursor.lastrowid
        conn.close()
        
        # Send email
        server = smtplib.SMTP('smtp.gmail.com', 587)
        server.starttls()
        server.login("app@example.com", "email_password")
        message = f"Welcome {name}!"
        server.sendmail("app@example.com", email, message)
        server.quit()
        
        return True, user_id

"""
ÉTAPE 1 : CHARACTERIZATION TESTS
─────────────────────────────────
"""

# test_legacy.py
def test_register_characterization():
    """Tests pour documenter comportement actuel"""
    reg = UserRegistration()
    
    # Note : Ces tests échoueront (DB/SMTP)
    # Mais documentent ce que le code FAIT
    
    # Email invalide
    success, msg = reg.register("invalid", "password123", "Alice")
    assert success == False
    assert "email" in msg.lower()
    
    # Password trop court
    success, msg = reg.register("alice@example.com", "short", "Alice")
    assert success == False
    assert "password" in msg.lower()

"""
ÉTAPE 2 : DEPENDENCY INJECTION
───────────────────────────────
"""

# refactored.py
class UserRegistration:
    def __init__(self, database=None, email_service=None):
        self.db = database
        self.email_service = email_service
    
    def register(self, email, password, name):
        # Validation (extrait)
        error = self._validate(email, password)
        if error:
            return False, error
        
        # Check duplicate
        if self.db.user_exists(email):
            return False, "Email exists"
        
        # Insert
        user_id = self.db.create_user(email, password, name)
        
        # Send email
        self.email_service.send_welcome(email, name)
        
        return True, user_id
    
    def _validate(self, email, password):
        """Extrait pour testabilité"""
        if not email or '@' not in email:
            return "Invalid email"
        if len(password) < 8:
            return "Password too short"
        return None

"""
ÉTAPE 3 : TESTS AVEC MOCKS
───────────────────────────
"""

from unittest.mock import Mock
import pytest

def test_register_success():
    # Mocks
    mock_db = Mock()
    mock_db.user_exists.return_value = False
    mock_db.create_user.return_value = 123
    
    mock_email = Mock()
    
    # Test
    reg = UserRegistration(mock_db, mock_email)
    success, user_id = reg.register("alice@example.com", "password123", "Alice")
    
    # Vérifications
    assert success == True
    assert user_id == 123
    mock_db.create_user.assert_called_once_with(
        "alice@example.com", "password123", "Alice"
    )
    mock_email.send_welcome.assert_called_once_with(
        "alice@example.com", "Alice"
    )

def test_register_invalid_email():
    mock_db = Mock()
    mock_email = Mock()
    
    reg = UserRegistration(mock_db, mock_email)
    success, msg = reg.register("invalid", "password123", "Alice")
    
    assert success == False
    assert "email" in msg.lower()
    # DB/Email ne doivent PAS être appelés
    mock_db.create_user.assert_not_called()
    mock_email.send_welcome.assert_not_called()

def test_register_duplicate_email():
    mock_db = Mock()
    mock_db.user_exists.return_value = True  # Existe déjà
    mock_email = Mock()
    
    reg = UserRegistration(mock_db, mock_email)
    success, msg = reg.register("alice@example.com", "password123", "Alice")
    
    assert success == False
    assert "exists" in msg.lower()
    mock_db.create_user.assert_not_called()

"""
ÉTAPE 4 : REFACTORING CONTINU (TDD)
────────────────────────────────────

Maintenant qu'on a tests, on peut refactorer :
"""

class EmailValidator:
    """Nouvelle classe extraite (TDD)"""
    @staticmethod
    def validate(email):
        if not email:
            raise ValueError("Email required")
        if '@' not in email:
            raise ValueError("Invalid email format")
        return True

class PasswordValidator:
    """Nouvelle classe extraite (TDD)"""
    def __init__(self, min_length=8):
        self.min_length = min_length
    
    def validate(self, password):
        if len(password) < self.min_length:
            raise ValueError(f"Password must be at least {self.min_length} characters")
        return True

# Tests pour nouvelles classes
def test_email_validator():
    with pytest.raises(ValueError, match="Email required"):
        EmailValidator.validate("")
    
    with pytest.raises(ValueError, match="Invalid email"):
        EmailValidator.validate("invalid")
    
    assert EmailValidator.validate("alice@example.com") == True

# Intégrer dans UserRegistration
class UserRegistration:
    def __init__(self, database, email_service):
        self.db = database
        self.email_service = email_service
        self.email_validator = EmailValidator()
        self.password_validator = PasswordValidator(min_length=8)
    
    def register(self, email, password, name):
        # Validation avec nouvelles classes
        try:
            self.email_validator.validate(email)
            self.password_validator.validate(password)
        except ValueError as e:
            return False, str(e)
        
        # Reste du code...
        if self.db.user_exists(email):
            return False, "Email exists"
        
        user_id = self.db.create_user(email, password, name)
        self.email_service.send_welcome(email, name)
        
        return True, user_id

"""
[OK] RÉSULTAT FINAL

- Code testé à 100%
- Refactorisé progressivement
- Toujours fonctionnel
- Dépendances injectées
- Classes à responsabilité unique
- Prêt pour évolution future
"""

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Définition code legacy
   Code sans tests

[OK] Characterization tests
   Documenter comportement actuel

[OK] Techniques d'injection
   - Dependency injection
   - Extract and override
   - Seam

[OK] Stratégies progressives
   - Sprout method/class
   - Wrap method/class
   - Strangler fig

[OK] Cas pratique complet
   UserRegistration refactorisé


[CLE] POINTS CLÉS

1. Legacy ≠ Mauvais
   Juste sans tests

2. Tests d'abord
   Avant toute modification

3. Progressif
   Pas de big bang

4. Toujours fonctionnel
   Jamais tout casser

5. TDD pour nouveau code
   Refactoring avec tests


[THOUGHT_BALLOON] CITATIONS

"Code without tests is bad code.
It doesn't matter how well written it is."
- Michael Feathers

"Legacy code is code without tests."
- Michael Feathers, Working Effectively with Legacy Code


[DOCS] RESSOURCES

Livre essentiel :
"Working Effectively with Legacy Code" - Michael Feathers


[RAPIDE] PROCHAINE ÉTAPE

Chapitre 10 : Design Patterns TDD
Patterns émergents du TDD
"""

# ============================================================================
# FIN CHAPITRE 9 - SUITE DANS FICHIER SUIVANT
# ============================================================================

# ============================================================================
# [LIVRE] TDD - PARTIE 2 (FIN) : DESIGN PATTERNS, DB ET APIS
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 10 : DESIGN PATTERNS TDD
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Patterns émergents du TDD
[OK] Builder pattern pour tests
[OK] Object Mother pattern
[OK] Test Data Builder
[OK] Fixture patterns
"""

# ----------------------------------------------------------------------------
# [DESIGN] PATTERNS ÉMERGENTS DU TDD
# ----------------------------------------------------------------------------

"""
TDD POUSSE VERS SOLID
─────────────────────

TDD -> Testabilité -> SOLID principles

S - Single Responsibility
O - Open/Closed
L - Liskov Substitution
I - Interface Segregation
D - Dependency Inversion


EXEMPLE : SINGLE RESPONSIBILITY
────────────────────────────────
"""

# [X] Classe fait trop de choses (difficile à tester)
class UserManager:
    def create_user(self, data):
        # Validation
        if not self._valid_email(data['email']):
            raise ValueError("Invalid email")
        
        # Hashing password
        hashed = self._hash_password(data['password'])
        
        # Sauvegarde DB
        user_id = self.db.insert(data)
        
        # Envoi email
        self._send_welcome_email(data['email'])
        
        # Logging
        self._log_user_creation(user_id)
        
        return user_id

# [OK] TDD pousse vers séparation (facile à tester)
class EmailValidator:
    """Une seule responsabilité"""
    def validate(self, email):
        return '@' in email and '.' in email

class PasswordHasher:
    """Une seule responsabilité"""
    def hash(self, password):
        import hashlib
        return hashlib.sha256(password.encode()).hexdigest()

class UserRepository:
    """Une seule responsabilité"""
    def save(self, user):
        return self.db.insert(user)

class WelcomeEmailSender:
    """Une seule responsabilité"""
    def send(self, email):
        # ...
        pass

class UserCreationService:
    """Orchestration uniquement"""
    def __init__(self, validator, hasher, repo, emailer):
        self.validator = validator
        self.hasher = hasher
        self.repo = repo
        self.emailer = emailer
    
    def create_user(self, email, password):
        # Chaque étape testable séparément
        self.validator.validate(email)
        hashed = self.hasher.hash(password)
        user = User(email, hashed)
        user_id = self.repo.save(user)
        self.emailer.send(email)
        return user_id

"""
[IDEE] TDD = Design feedback

Tests difficiles -> Code couplé
Tests faciles -> Bon design


EXEMPLE : DEPENDENCY INVERSION
───────────────────────────────
"""

# [X] Dépend de concrétions (difficile à tester)
class OrderProcessor:
    def __init__(self):
        self.db = MySQLDatabase()  # Concrétion
        self.payment = StripeGateway()  # Concrétion
    
    def process(self, order):
        self.db.save(order)
        self.payment.charge(order.total)

# [OK] TDD pousse vers abstractions (facile à tester)
from abc import ABC, abstractmethod

class Database(ABC):
    """Abstraction"""
    @abstractmethod
    def save(self, entity):
        pass

class PaymentGateway(ABC):
    """Abstraction"""
    @abstractmethod
    def charge(self, amount):
        pass

class OrderProcessor:
    def __init__(self, database: Database, payment: PaymentGateway):
        # Dépend d'abstractions
        self.db = database
        self.payment = payment
    
    def process(self, order):
        self.db.save(order)
        self.payment.charge(order.total)

# Tests faciles avec mocks
def test_order_processor():
    mock_db = Mock(spec=Database)
    mock_payment = Mock(spec=PaymentGateway)
    
    processor = OrderProcessor(mock_db, mock_payment)
    order = Order(total=100)
    
    processor.process(order)
    
    mock_db.save.assert_called_once_with(order)
    mock_payment.charge.assert_called_once_with(100)

# ----------------------------------------------------------------------------
# [CONSTRUCTION] BUILDER PATTERN POUR TESTS
# ----------------------------------------------------------------------------

"""
PROBLÈME : OBJETS COMPLEXES DANS TESTS
───────────────────────────────────────

Tests avec objets complexes :
"""

def test_order_processing():
    # Création laborieuse
    address = Address(
        street="123 Main St",
        city="Paris",
        postal_code="75001",
        country="France"
    )
    
    customer = Customer(
        id=1,
        name="Alice",
        email="alice@example.com",
        address=address,
        vip=True,
        created_at=datetime.now()
    )
    
    items = [
        OrderItem(
            product_id=1,
            name="Book",
            price=10,
            quantity=2
        ),
        OrderItem(
            product_id=2,
            name="Pen",
            price=1,
            quantity=5
        )
    ]
    
    order = Order(
        customer=customer,
        items=items,
        created_at=datetime.now(),
        status="pending"
    )
    
    # Test enfin !
    result = process_order(order)
    assert result.status == "completed"

"""
[X] PROBLÈMES

- Beaucoup de code setup
- Difficile à lire
- Répétition dans chaque test
- Focus perdu


SOLUTION : BUILDER PATTERN
───────────────────────────
"""

class OrderBuilder:
    """Builder pour créer Order dans tests"""
    
    def __init__(self):
        self._customer = None
        self._items = []
        self._status = "pending"
    
    def with_customer(self, name="Default", email="default@example.com", vip=False):
        """Configure customer"""
        address = Address("123 Main", "Paris", "75001", "France")
        self._customer = Customer(
            id=1,
            name=name,
            email=email,
            address=address,
            vip=vip
        )
        return self
    
    def with_item(self, name="Product", price=10, quantity=1):
        """Ajoute item"""
        self._items.append(OrderItem(
            product_id=len(self._items) + 1,
            name=name,
            price=price,
            quantity=quantity
        ))
        return self
    
    def with_status(self, status):
        """Configure status"""
        self._status = status
        return self
    
    def build(self):
        """Construit Order"""
        if not self._customer:
            self.with_customer()  # Valeurs par défaut
        
        if not self._items:
            self.with_item()  # Item par défaut
        
        return Order(
            customer=self._customer,
            items=self._items,
            status=self._status,
            created_at=datetime.now()
        )

"""
UTILISATION DANS TESTS
───────────────────────
"""

def test_order_processing_with_builder():
    # [OK] Concis et lisible
    order = (OrderBuilder()
             .with_customer(name="Alice", vip=True)
             .with_item(name="Book", price=10, quantity=2)
             .with_item(name="Pen", price=1, quantity=5)
             .build())
    
    result = process_order(order)
    assert result.status == "completed"

def test_order_with_defaults():
    # [OK] Encore plus simple
    order = OrderBuilder().build()
    
    result = process_order(order)
    assert result.status == "completed"

def test_vip_order():
    # [OK] Focus sur ce qui compte
    order = (OrderBuilder()
             .with_customer(vip=True)
             .build())
    
    result = process_order(order)
    assert result.discount > 0

"""
[IDEE] AVANTAGES BUILDER

[OK] Code test concis
[OK] Lisibilité
[OK] Réutilisation
[OK] Valeurs par défaut
[OK] Fluent API (chainable)
"""

# ----------------------------------------------------------------------------
# [FAMILY] OBJECT MOTHER PATTERN
# ----------------------------------------------------------------------------

"""
OBJECT MOTHER
─────────────

Pattern : Fabrique d'objets prêts à l'emploi pour tests


IMPLÉMENTATION
──────────────
"""

class UserMother:
    """Object Mother pour User"""
    
    @staticmethod
    def default():
        """User standard"""
        return User(
            id=1,
            name="John Doe",
            email="john@example.com",
            active=True
        )
    
    @staticmethod
    def vip():
        """User VIP"""
        return User(
            id=2,
            name="Alice VIP",
            email="alice@example.com",
            active=True,
            vip=True,
            vip_since=datetime(2020, 1, 1)
        )
    
    @staticmethod
    def inactive():
        """User inactif"""
        return User(
            id=3,
            name="Bob Inactive",
            email="bob@example.com",
            active=False
        )
    
    @staticmethod
    def with_email(email):
        """User avec email personnalisé"""
        user = UserMother.default()
        user.email = email
        return user

"""
UTILISATION
───────────
"""

def test_send_email_to_active_user():
    user = UserMother.default()  # [OK] Simple
    
    result = email_service.send(user, "Hello")
    assert result.success

def test_vip_discount():
    user = UserMother.vip()  # [OK] Clair
    
    discount = calculate_discount(user)
    assert discount == 20

def test_cannot_email_inactive():
    user = UserMother.inactive()  # [OK] Explicite
    
    with pytest.raises(ValueError):
        email_service.send(user, "Hello")

"""
COMBINER AVEC BUILDER
─────────────────────

Object Mother pour cas communs
Builder pour personnalisation
"""

class OrderMother:
    """Object Mother pour Order"""
    
    @staticmethod
    def standard():
        return (OrderBuilder()
                .with_customer("Standard Customer")
                .with_item("Standard Product", 10, 1)
                .build())
    
    @staticmethod
    def large():
        """Grosse commande"""
        builder = OrderBuilder()
        builder.with_customer("Big Customer")
        for i in range(10):
            builder.with_item(f"Product {i}", 50, 2)
        return builder.build()
    
    @staticmethod
    def vip():
        return (OrderBuilder()
                .with_customer(vip=True)
                .with_item("Premium Product", 100, 1)
                .build())

# ----------------------------------------------------------------------------
# [PACKAGE] TEST DATA BUILDER
# ----------------------------------------------------------------------------

"""
TEST DATA BUILDER
─────────────────

Variante du Builder spécialisée pour données de test


EXEMPLE : GÉNÉRATION DONNÉES RÉALISTES
───────────────────────────────────────
"""

import random
import string
from datetime import datetime, timedelta

class TestDataBuilder:
    """Builder pour données de test réalistes"""
    
    @staticmethod
    def random_email():
        """Génère email aléatoire"""
        username = ''.join(random.choices(string.ascii_lowercase, k=8))
        return f"{username}@test.com"
    
    @staticmethod
    def random_date(days_ago=30):
        """Date aléatoire dans le passé"""
        days = random.randint(0, days_ago)
        return datetime.now() - timedelta(days=days)
    
    @staticmethod
    def random_price(min_price=1, max_price=1000):
        """Prix aléatoire"""
        return round(random.uniform(min_price, max_price), 2)

class ProductBuilder:
    """Builder avec données réalistes"""
    
    def __init__(self):
        self.id = random.randint(1, 10000)
        self.name = f"Product-{self.id}"
        self.price = TestDataBuilder.random_price()
        self.created_at = TestDataBuilder.random_date()
    
    def with_name(self, name):
        self.name = name
        return self
    
    def with_price(self, price):
        self.price = price
        return self
    
    def build(self):
        return Product(
            id=self.id,
            name=self.name,
            price=self.price,
            created_at=self.created_at
        )

"""
UTILISATION
───────────
"""

def test_with_random_data():
    # Données réalistes automatiques
    product = ProductBuilder().build()
    
    # Test logique métier
    discount = calculate_discount(product)
    assert 0 <= discount <= product.price

def test_many_products():
    # Générer plusieurs produits facilement
    products = [ProductBuilder().build() for _ in range(100)]
    
    total = sum(p.price for p in products)
    assert total > 0

# ----------------------------------------------------------------------------
# [OUTIL] FIXTURE PATTERNS
# ----------------------------------------------------------------------------

"""
FIXTURES pytest
───────────────

Fixtures = Setup partagé entre tests


FIXTURE SIMPLE
──────────────
"""

import pytest

@pytest.fixture
def calculator():
    """Fixture calculatrice"""
    return Calculator()

def test_addition(calculator):
    assert calculator.add(2, 3) == 5

def test_subtraction(calculator):
    assert calculator.subtract(5, 2) == 3

"""
FIXTURE AVEC SETUP/TEARDOWN
────────────────────────────
"""

@pytest.fixture
def database():
    """Fixture DB avec setup et teardown"""
    # Setup
    db = TestDatabase()
    db.connect()
    db.create_tables()
    
    yield db  # Fournie au test
    
    # Teardown (après le test)
    db.drop_tables()
    db.disconnect()

def test_save_user(database):
    user = User("Alice")
    database.save(user)
    
    found = database.get_user("Alice")
    assert found.name == "Alice"

"""
FIXTURE PARAMÉTRÉE
──────────────────
"""

@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database_type(request):
    """Teste avec plusieurs types de DB"""
    return request.param

def test_database_compatibility(database_type):
    db = Database(database_type)
    # Test fonctionne avec tous les types
    assert db.connect()

"""
FIXTURE SCOPE
─────────────
"""

@pytest.fixture(scope="module")
def expensive_resource():
    """Créée une fois par module (pas par test)"""
    # Setup coûteux
    resource = ExpensiveResource()
    resource.initialize()
    
    yield resource
    
    # Teardown une seule fois
    resource.cleanup()

@pytest.fixture(scope="session")
def app_config():
    """Créée une fois pour toute la session de tests"""
    return load_config()

"""
FIXTURE FACTORIES
─────────────────

Fixture qui retourne une factory
"""

@pytest.fixture
def user_factory():
    """Factory pour créer users dans tests"""
    users_created = []
    
    def _create_user(name="Default", email=None):
        email = email or f"{name.lower()}@test.com"
        user = User(name=name, email=email)
        users_created.append(user)
        return user
    
    yield _create_user
    
    # Cleanup tous les users créés
    for user in users_created:
        user.delete()

def test_with_factory(user_factory):
    # Créer plusieurs users facilement
    alice = user_factory("Alice")
    bob = user_factory("Bob", "bob@custom.com")
    
    assert alice.email == "alice@test.com"
    assert bob.email == "bob@custom.com"

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] TDD pousse vers SOLID
   Design émergent naturellement

[OK] Builder pattern
   Objets complexes dans tests

[OK] Object Mother
   Objets prêts à l'emploi

[OK] Test Data Builder
   Données réalistes

[OK] Fixture patterns
   Setup partagé, factories


[CLE] POINTS CLÉS

1. TDD = Design tool
   Feedback sur architecture

2. Builders = Lisibilité
   Tests concis et clairs

3. Object Mother = Réutilisation
   Cas communs centralisés

4. Fixtures = DRY
   Éviter duplication setup


[RAPIDE] PROCHAINE ÉTAPE

Chapitre 11 : TDD avec Bases de Données
Tests avec DB réelles
"""


# ============================================================================
# [GUIDE] CHAPITRE 11 : TDD AVEC BASES DE DONNÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Stratégies de test avec DB
[OK] Utiliser DB de test
[OK] Transactions et rollback
[OK] Fixtures de données
[OK] Tests de migrations
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] STRATÉGIES DE TEST AVEC DB
# ----------------------------------------------------------------------------

"""
3 APPROCHES
───────────

1. DB EN MÉMOIRE (SQLite)
   [OK] Rapide
   [X] Pas exactement comme prod

2. DB DOCKER (Postgres/MySQL)
   [OK] Réaliste
   [X] Plus lent

3. DB CLOUD (instance test)
   [OK] Exactement comme prod
   [X] Coûteux, lent


RECOMMANDATION
──────────────

- Tests unitaires : Mocks (pas de DB)
- Tests intégration : DB en mémoire
- Tests E2E : DB Docker/Cloud


EXEMPLE : PYRAMIDE AVEC DB
───────────────────────────
"""

# Tests unitaires (70%) : Mocks
def test_user_service_unit():
    mock_repo = Mock()
    mock_repo.save.return_value = User(id=1)
    
    service = UserService(mock_repo)
    user = service.create_user("Alice")
    
    mock_repo.save.assert_called_once()

# Tests intégration (20%) : SQLite
def test_user_service_integration():
    db = SQLiteDatabase(':memory:')
    repo = UserRepository(db)
    service = UserService(repo)
    
    user = service.create_user("Alice")
    
    found = repo.find_by_id(user.id)
    assert found.name == "Alice"

# Tests E2E (10%) : PostgreSQL
def test_user_service_e2e():
    db = PostgresDatabase('test_db')
    repo = UserRepository(db)
    service = UserService(repo)
    
    user = service.create_user("Alice")
    
    # Vérifier avec vraie DB
    found = repo.find_by_id(user.id)
    assert found.name == "Alice"

# ----------------------------------------------------------------------------
# [ARCHIVE] CONFIGURATION DB DE TEST
# ----------------------------------------------------------------------------

"""
SQLITE EN MÉMOIRE (Python)
───────────────────────────
"""

import sqlite3
from contextlib import contextmanager

class TestDatabase:
    def __init__(self):
        self.conn = sqlite3.connect(':memory:')
        self.setup_schema()
    
    def setup_schema(self):
        cursor = self.conn.cursor()
        cursor.execute('''
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL
            )
        ''')
        self.conn.commit()
    
    def close(self):
        self.conn.close()

@pytest.fixture
def test_db():
    db = TestDatabase()
    yield db
    db.close()

def test_with_sqlite(test_db):
    cursor = test_db.conn.cursor()
    cursor.execute(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        ("Alice", "alice@example.com")
    )
    test_db.conn.commit()
    
    cursor.execute("SELECT * FROM users WHERE name = ?", ("Alice",))
    user = cursor.fetchone()
    assert user[1] == "Alice"

"""
POSTGRESQL AVEC DOCKER
──────────────────────
"""

import psycopg2
import subprocess

@pytest.fixture(scope="session")
def postgres_db():
    """Lance container PostgreSQL pour tests"""
    # Démarrer container
    subprocess.run([
        "docker", "run", "-d",
        "--name", "test-postgres",
        "-e", "POSTGRES_PASSWORD=test",
        "-p", "5433:5432",
        "postgres:14"
    ])
    
    # Attendre que DB soit prête
    import time
    time.sleep(3)
    
    # Connexion
    conn = psycopg2.connect(
        host="localhost",
        port=5433,
        user="postgres",
        password="test",
        database="postgres"
    )
    
    yield conn
    
    # Cleanup
    conn.close()
    subprocess.run(["docker", "stop", "test-postgres"])
    subprocess.run(["docker", "rm", "test-postgres"])

"""
SQLAlchemy (ORM)
────────────────
"""

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import Base  # Vos modèles

@pytest.fixture
def db_session():
    """Session SQLAlchemy pour tests"""
    # DB en mémoire
    engine = create_engine('sqlite:///:memory:')
    
    # Créer tables
    Base.metadata.create_all(engine)
    
    # Session
    Session = sessionmaker(bind=engine)
    session = Session()
    
    yield session
    
    # Cleanup
    session.close()

def test_with_sqlalchemy(db_session):
    user = User(name="Alice", email="alice@example.com")
    db_session.add(user)
    db_session.commit()
    
    found = db_session.query(User).filter_by(name="Alice").first()
    assert found.email == "alice@example.com"

# ----------------------------------------------------------------------------
# [SYNC] TRANSACTIONS ET ROLLBACK
# ----------------------------------------------------------------------------

"""
PATTERN : TRANSACTION PAR TEST
───────────────────────────────

Chaque test dans une transaction
Rollback après test = Cleanup automatique
"""

@pytest.fixture
def db_session_with_rollback():
    """Session avec rollback automatique"""
    engine = create_engine('postgresql://localhost/test_db')
    connection = engine.connect()
    transaction = connection.begin()
    
    Session = sessionmaker(bind=connection)
    session = Session()
    
    yield session
    
    # Rollback (annule toutes les modifications)
    session.close()
    transaction.rollback()
    connection.close()

def test_user_creation(db_session_with_rollback):
    # Créer user
    user = User(name="Alice")
    db_session_with_rollback.add(user)
    db_session_with_rollback.commit()
    
    # Test
    assert user.id is not None
    
    # Après le test : rollback automatique
    # User n'existe plus en DB !

"""
[IDEE] AVANTAGES

[OK] Tests isolés (pas d'effets de bord)
[OK] Pas de cleanup manuel
[OK] Tests parallélisables
[OK] Rapide


PATTERN : SAVEPOINT
───────────────────

Pour tests imbriqués
"""

def test_with_savepoint(db_session):
    # Savepoint
    sp = db_session.begin_nested()
    
    # Modifications
    user = User(name="Alice")
    db_session.add(user)
    db_session.flush()
    
    # Rollback au savepoint
    sp.rollback()
    
    # User n'existe pas
    assert db_session.query(User).count() == 0

# ----------------------------------------------------------------------------
# [PACKAGE] FIXTURES DE DONNÉES
# ----------------------------------------------------------------------------

"""
DONNÉES DE TEST RÉUTILISABLES
──────────────────────────────
"""

@pytest.fixture
def sample_users(db_session):
    """Crée users de test"""
    users = [
        User(name="Alice", email="alice@example.com"),
        User(name="Bob", email="bob@example.com"),
        User(name="Charlie", email="charlie@example.com")
    ]
    
    for user in users:
        db_session.add(user)
    
    db_session.commit()
    return users

def test_find_users(db_session, sample_users):
    # Données déjà présentes
    found = db_session.query(User).all()
    assert len(found) == 3

"""
FACTORY BOY (Python)
────────────────────

Bibliothèque pour générer données test
"""

# pip install factory-boy

import factory
from factory.alchemy import SQLAlchemyModelFactory

class UserFactory(SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session = None  # Défini dans fixture
    
    name = factory.Sequence(lambda n: f"User{n}")
    email = factory.LazyAttribute(lambda obj: f"{obj.name.lower()}@example.com")
    created_at = factory.Faker('date_time_this_year')

# Configuration session
@pytest.fixture
def user_factory(db_session):
    UserFactory._meta.sqlalchemy_session = db_session
    return UserFactory

# Utilisation
def test_with_factory(user_factory):
    # Créer users facilement
    alice = user_factory(name="Alice")
    bob = user_factory(name="Bob")
    
    # Génération automatique
    users = user_factory.create_batch(10)
    
    assert len(users) == 10

"""
FAKER (Données réalistes)
─────────────────────────
"""

from faker import Faker

fake = Faker()

@pytest.fixture
def realistic_user(db_session):
    """User avec données réalistes"""
    user = User(
        name=fake.name(),
        email=fake.email(),
        phone=fake.phone_number(),
        address=fake.address(),
        birthdate=fake.date_of_birth()
    )
    db_session.add(user)
    db_session.commit()
    return user

# ----------------------------------------------------------------------------
# [MELANGE] TESTS DE MIGRATIONS
# ----------------------------------------------------------------------------

"""
TESTER MIGRATIONS DB
────────────────────

Alembic (SQLAlchemy) ou Django migrations


EXEMPLE ALEMBIC
───────────────
"""

def test_migration_up():
    """Tester migration vers nouvelle version"""
    from alembic import command
    from alembic.config import Config
    
    alembic_cfg = Config("alembic.ini")
    
    # Appliquer migration
    command.upgrade(alembic_cfg, "head")
    
    # Vérifier structure
    inspector = inspect(engine)
    columns = [c['name'] for c in inspector.get_columns('users')]
    
    # Nouvelle colonne existe ?
    assert 'phone_number' in columns

def test_migration_down():
    """Tester rollback migration"""
    from alembic import command
    from alembic.config import Config
    
    alembic_cfg = Config("alembic.ini")
    
    # Rollback
    command.downgrade(alembic_cfg, "-1")
    
    # Vérifier structure
    inspector = inspect(engine)
    columns = [c['name'] for c in inspector.get_columns('users')]
    
    # Colonne n'existe plus
    assert 'phone_number' not in columns

"""
TEST DONNÉES APRÈS MIGRATION
─────────────────────────────
"""

def test_migration_preserves_data():
    """Vérifier que migration garde les données"""
    # Avant migration
    db_session.add(User(name="Alice", email="alice@example.com"))
    db_session.commit()
    
    # Appliquer migration
    command.upgrade(alembic_cfg, "head")
    
    # Après migration
    user = db_session.query(User).filter_by(name="Alice").first()
    assert user is not None
    assert user.email == "alice@example.com"

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Stratégies test DB
   - SQLite mémoire
   - Docker containers
   - DB cloud

[OK] Configuration DB test
   - Fixtures
   - Setup/teardown

[OK] Transactions
   - Rollback automatique
   - Isolation tests

[OK] Fixtures données
   - Factory Boy
   - Faker

[OK] Tests migrations
   - Alembic
   - Préservation données


[CLE] POINTS CLÉS

1. Pyramide respectée
   Peu de tests avec vraie DB

2. SQLite pour vitesse
   PostgreSQL pour réalisme

3. Rollback = Isolation
   Tests indépendants

4. Factory = Productivité
   Génération données facile


[RAPIDE] PROCHAINE ÉTAPE

Chapitre 12 : TDD avec APIs
Tester appels API externes
"""


# ============================================================================
# [GUIDE] CHAPITRE 12 : TDD AVEC APIs
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Tester APIs REST
[OK] Mocker appels HTTP
[OK] Tester erreurs réseau
[OK] Tests contrat API
[OK] VCR.py pour enregistrer requêtes
"""

# ----------------------------------------------------------------------------
# [WEB] TESTER APPELS API EXTERNES
# ----------------------------------------------------------------------------

"""
PROBLÈMES TESTS AVEC APIS
─────────────────────────

[X] Lent (appels réseau)
[X] Coûteux (quotas API)
[X] Flaky (dépend réseau)
[X] Données changeantes
[X] Erreurs imprévisibles


STRATÉGIE
─────────

Tests unitaires -> Mock HTTP
Tests intégration -> API de test/staging
Tests E2E -> API réelle (peu de tests)


CODE À TESTER
─────────────
"""

import requests

class WeatherAPI:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://api.weather.com/v3"
    
    def get_current(self, city):
        """Obtenir météo actuelle"""
        response = requests.get(
            f"{self.base_url}/weather/current",
            params={
                'city': city,
                'apikey': self.api_key
            }
        )
        
        if response.status_code != 200:
            raise APIError(f"API error: {response.status_code}")
        
        data = response.json()
        return {
            'temperature': data['temperature'],
            'conditions': data['conditions'],
            'humidity': data['humidity']
        }

# ----------------------------------------------------------------------------
# [SCENARIO] MOCKER APPELS HTTP
# ----------------------------------------------------------------------------

"""
AVEC requests-mock
──────────────────
"""

# pip install requests-mock

import requests_mock
import pytest

def test_get_current_weather():
    api = WeatherAPI(api_key="test-key")
    
    with requests_mock.Mocker() as m:
        # Mock la réponse
        m.get(
            "https://api.weather.com/v3/weather/current",
            json={
                'temperature': 20,
                'conditions': 'Sunny',
                'humidity': 60
            }
        )
        
        # Test
        weather = api.get_current("Paris")
        
        # Vérifications
        assert weather['temperature'] == 20
        assert weather['conditions'] == 'Sunny'

def test_api_error_handling():
    api = WeatherAPI(api_key="test-key")
    
    with requests_mock.Mocker() as m:
        # Mock erreur 500
        m.get(
            "https://api.weather.com/v3/weather/current",
            status_code=500
        )
        
        # Vérifier exception
        with pytest.raises(APIError):
            api.get_current("Paris")

"""
AVEC unittest.mock
──────────────────
"""

from unittest.mock import patch, Mock

@patch('requests.get')
def test_get_weather_with_mock(mock_get):
    # Configurer mock
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        'temperature': 20,
        'conditions': 'Sunny',
        'humidity': 60
    }
    mock_get.return_value = mock_response
    
    # Test
    api = WeatherAPI(api_key="test-key")
    weather = api.get_current("Paris")
    
    # Vérifications
    assert weather['temperature'] == 20
    
    # Vérifier que requests.get a été appelé correctement
    mock_get.assert_called_once()
    call_args = mock_get.call_args
    assert 'Paris' in str(call_args)

"""
AVEC responses
──────────────
"""

# pip install responses

import responses

@responses.activate
def test_with_responses():
    # Enregistrer mock
    responses.add(
        responses.GET,
        "https://api.weather.com/v3/weather/current",
        json={'temperature': 20, 'conditions': 'Sunny', 'humidity': 60},
        status=200
    )
    
    # Test
    api = WeatherAPI(api_key="test-key")
    weather = api.get_current("Paris")
    
    assert weather['temperature'] == 20
    
    # Vérifier requête
    assert len(responses.calls) == 1
    assert responses.calls[0].request.url.startswith("https://api.weather.com")

# ----------------------------------------------------------------------------
# [VIDEOCASSETTE] VCR.py : ENREGISTRER REQUÊTES
# ----------------------------------------------------------------------------

"""
VCR.py
──────

Enregistre requêtes HTTP réelles
Rejoue enregistrement dans tests


INSTALLATION
────────────
"""

# pip install vcrpy pytest-vcr

"""
UTILISATION
───────────
"""

import vcr

@vcr.use_cassette('fixtures/vcr_cassettes/weather_paris.yaml')
def test_weather_with_vcr():
    """
    Premier run : Appel API réel + enregistrement
    Runs suivants : Rejoue enregistrement (pas d'appel réel)
    """
    api = WeatherAPI(api_key="real-api-key")
    weather = api.get_current("Paris")
    
    assert weather['temperature'] > -50
    assert weather['temperature'] < 50

"""
[IDEE] AVANTAGES VCR

[OK] Tests réalistes (vraies réponses API)
[OK] Rapide (pas d'appel réseau après premier run)
[OK] Déterministe (toujours même réponse)
[OK] Pas de quotas consommés


CASSETTE YAML GÉNÉRÉE
──────────────────────
"""

# fixtures/vcr_cassettes/weather_paris.yaml
"""
interactions:
- request:
    method: GET
    uri: https://api.weather.com/v3/weather/current?city=Paris&apikey=***
  response:
    status:
      code: 200
    body:
      string: '{"temperature":20,"conditions":"Sunny","humidity":60}'
    headers:
      Content-Type:
      - application/json
"""

"""
RE-ENREGISTRER
──────────────

Pour mettre à jour cassette :
1. Supprimer fichier .yaml
2. Relancer test -> Nouvel enregistrement
"""

# ----------------------------------------------------------------------------
# [LISTE] TESTS CONTRAT API
# ----------------------------------------------------------------------------

"""
CONTRACT TESTING
────────────────

Vérifier que API respecte contrat attendu


EXEMPLE : SCHÉMA VALIDATION
────────────────────────────
"""

# pip install jsonschema

from jsonschema import validate, ValidationError

def test_weather_api_contract():
    """Vérifier schéma réponse API"""
    
    # Schéma attendu
    schema = {
        "type": "object",
        "properties": {
            "temperature": {"type": "number"},
            "conditions": {"type": "string"},
            "humidity": {"type": "number"}
        },
        "required": ["temperature", "conditions", "humidity"]
    }
    
    # Mock réponse
    response = {
        "temperature": 20,
        "conditions": "Sunny",
        "humidity": 60
    }
    
    # Valider
    validate(instance=response, schema=schema)  # Lève erreur si invalide

def test_api_contract_broken():
    """Détecter rupture contrat"""
    schema = {
        "type": "object",
        "properties": {"temperature": {"type": "number"}},
        "required": ["temperature"]
    }
    
    # Réponse sans temperature
    response = {"conditions": "Sunny"}
    
    with pytest.raises(ValidationError):
        validate(instance=response, schema=schema)

"""
PACT (Consumer-Driven Contracts)
─────────────────────────────────

Framework pour contrats entre services
"""

# pip install pact-python

from pact import Consumer, Provider

pact = Consumer('WeatherApp').has_pact_with(Provider('WeatherAPI'))

def test_pact_weather_api():
    # Définir contrat
    (pact
     .given('weather data exists for Paris')
     .upon_receiving('a request for Paris weather')
     .with_request('get', '/v3/weather/current', query='city=Paris')
     .will_respond_with(200, body={
         'temperature': 20,
         'conditions': 'Sunny'
     }))
    
    # Tester
    with pact:
        api = WeatherAPI(api_key="test")
        weather = api.get_current("Paris")
        assert weather['temperature'] == 20

# ----------------------------------------------------------------------------
# [SYNC] TESTER RETRY ET TIMEOUT
# ----------------------------------------------------------------------------

"""
RETRY LOGIC
───────────
"""

import time
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

class ResilientWeatherAPI:
    def __init__(self, api_key):
        self.api_key = api_key
        self.session = self._create_session()
    
    def _create_session(self):
        """Session avec retry automatique"""
        session = requests.Session()
        
        retry = Retry(
            total=3,
            backoff_factor=0.3,
            status_forcelist=[500, 502, 503, 504]
        )
        
        adapter = HTTPAdapter(max_retries=retry)
        session.mount('http://', adapter)
        session.mount('https://', adapter)
        
        return session
    
    def get_current(self, city, timeout=5):
        response = self.session.get(
            "https://api.weather.com/v3/weather/current",
            params={'city': city, 'apikey': self.api_key},
            timeout=timeout
        )
        return response.json()

"""
TESTS
─────
"""

@responses.activate
def test_retry_on_500():
    """Vérifie retry sur erreur 500"""
    api = ResilientWeatherAPI("test-key")
    
    # Première requête : 500
    responses.add(
        responses.GET,
        "https://api.weather.com/v3/weather/current",
        status=500
    )
    
    # Deuxième requête : 200
    responses.add(
        responses.GET,
        "https://api.weather.com/v3/weather/current",
        json={'temperature': 20},
        status=200
    )
    
    # Test
    weather = api.get_current("Paris")
    
    # Vérifier 2 requêtes faites
    assert len(responses.calls) == 2
    assert weather['temperature'] == 20

def test_timeout():
    """Tester timeout"""
    api = ResilientWeatherAPI("test-key")
    
    with responses.RequestsMock() as rsps:
        # Simuler timeout
        def timeout_callback(request):
            time.sleep(10)  # Plus long que timeout
            return (200, {}, '{}')
        
        rsps.add_callback(
            responses.GET,
            "https://api.weather.com/v3/weather/current",
            callback=timeout_callback
        )
        
        # Vérifier timeout exception
        with pytest.raises(requests.Timeout):
            api.get_current("Paris", timeout=1)

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Mocker appels HTTP
   - requests-mock
   - unittest.mock
   - responses

[OK] VCR.py
   - Enregistrer requêtes
   - Rejouer cassettes

[OK] Tests contrat
   - JSON Schema
   - Pact

[OK] Retry et timeout
   - Tester résilience
   - Gérer erreurs réseau


[CLE] POINTS CLÉS

1. Mock = Tests rapides
   Pas d'appels réels

2. VCR = Réalisme
   Vraies réponses enregistrées

3. Contrat = Stabilité
   API ne change pas sans prévenir

4. Retry = Résilience
   Gérer erreurs réseau


═══════════════════════════════════════════════════════════════
FÉLICITATIONS ! [BRAVO]

Vous avez complété la PARTIE 2 : PRATIQUE AVANCÉE

Vous savez maintenant :
[OK] Utiliser mocks et stubs
[OK] Tester code legacy
[OK] Appliquer design patterns TDD
[OK] Tester avec bases de données
[OK] Tester APIs externes

Vous êtes prêt pour la PARTIE 3 : MAÎTRISE !
═══════════════════════════════════════════════════════════════
"""

# ============================================================================
# FIN DE LA PARTIE 2 : PRATIQUE AVANCÉE
# SUITE DANS tdd_partie3.md
# ============================================================================

# ============================================================================
# [LIVRE] TDD - PARTIE 3 (FIN) : CI/CD, MÉTRIQUES ET BEST PRACTICES
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 15 : CI/CD ET TDD
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Intégrer TDD dans CI/CD
[OK] Configurer pipelines
[OK] Tests automatisés
[OK] Quality gates
[OK] Déploiement continu
"""

# ----------------------------------------------------------------------------
# [SYNC] CI/CD : DÉFINITIONS
# ----------------------------------------------------------------------------

"""
CONTINUOUS INTEGRATION (CI)
───────────────────────────

Définition : Intégrer code fréquemment (plusieurs fois/jour)
Tests automatiques à chaque commit


CONTINUOUS DELIVERY (CD)
────────────────────────

Définition : Code toujours déployable
Release manuel


CONTINUOUS DEPLOYMENT
─────────────────────

Définition : Déploiement automatique en production
Toutes modifs testées -> Production


TDD + CI/CD = PUISSANCE MAXIMALE
─────────────────────────────────

TDD garantit qualité code
CI/CD garantit intégration rapide
Ensemble = Feedback ultra-rapide
"""

# ----------------------------------------------------------------------------
# [CONFIG] GITHUB ACTIONS
# ----------------------------------------------------------------------------

"""
PIPELINE TDD COMPLET
────────────────────
"""

# .github/workflows/tests.yml
"""
name: Tests TDD

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        python-version: [3.9, 3.10, 3.11]
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install pytest pytest-cov
    
    - name: Run tests with coverage
      run: |
        pytest --cov=src --cov-report=xml --cov-report=html
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage.xml
        flags: unittests
        name: codecov-umbrella
    
    - name: Check coverage threshold
      run: |
        coverage report --fail-under=80
    
    - name: Archive test results
      if: always()
      uses: actions/upload-artifact@v3
      with:
        name: test-results
        path: htmlcov/
"""

"""
QUALITY GATES
─────────────

Empêcher merge si tests échouent ou coverage < seuil
"""

# .github/workflows/quality-gate.yml
"""
name: Quality Gate

on:
  pull_request:
    branches: [ main ]

jobs:
  quality:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Run tests
      run: pytest
    
    - name: Check coverage
      run: |
        pytest --cov=src --cov-report=term
        coverage report --fail-under=80
    
    - name: Static analysis
      run: |
        pip install pylint
        pylint src --fail-under=8.0
    
    - name: Security scan
      run: |
        pip install bandit
        bandit -r src
    
    - name: Comment PR
      if: failure()
      uses: actions/github-script@v6
      with:
        script: |
          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: '[X] Quality gate failed! Please fix issues.'
          })
"""

"""
TESTS PARALLÈLES
────────────────

Accélérer exécution
"""

# .github/workflows/parallel-tests.yml
"""
name: Parallel Tests

on: [push]

jobs:
  test-unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: pytest tests/unit
  
  test-integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: pytest tests/integration
  
  test-e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: pytest tests/e2e
"""

# ----------------------------------------------------------------------------
# [FOX_FACE] GITLAB CI
# ----------------------------------------------------------------------------

"""
PIPELINE GITLAB
───────────────
"""

# .gitlab-ci.yml
"""
stages:
  - test
  - quality
  - deploy

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

cache:
  paths:
    - .cache/pip
    - venv/

before_script:
  - python -m venv venv
  - source venv/bin/activate
  - pip install -r requirements.txt

test:unit:
  stage: test
  script:
    - pytest tests/unit -v --junitxml=report.xml
  artifacts:
    when: always
    reports:
      junit: report.xml

test:integration:
  stage: test
  script:
    - pytest tests/integration -v

test:coverage:
  stage: quality
  script:
    - pytest --cov=src --cov-report=html --cov-report=term
    - coverage report --fail-under=80
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    paths:
      - htmlcov/
    expire_in: 30 days

lint:
  stage: quality
  script:
    - pylint src --fail-under=8.0

security:
  stage: quality
  script:
    - bandit -r src

deploy:staging:
  stage: deploy
  script:
    - echo "Deploying to staging"
  only:
    - develop
  when: manual

deploy:production:
  stage: deploy
  script:
    - echo "Deploying to production"
  only:
    - main
  when: manual
"""

# ----------------------------------------------------------------------------
# [BLEU] AZURE DEVOPS
# ----------------------------------------------------------------------------

"""
PIPELINE AZURE
──────────────
"""

# azure-pipelines.yml
"""
trigger:
  branches:
    include:
      - main
      - develop

pool:
  vmImage: 'ubuntu-latest'

variables:
  pythonVersion: '3.10'

stages:
- stage: Test
  jobs:
  - job: UnitTests
    steps:
    - task: UsePythonVersion@0
      inputs:
        versionSpec: '$(pythonVersion)'
    
    - script: |
        pip install -r requirements.txt
        pip install pytest pytest-cov
      displayName: 'Install dependencies'
    
    - script: |
        pytest tests/unit --junitxml=junit/test-results.xml --cov=src --cov-report=xml
      displayName: 'Run unit tests'
    
    - task: PublishTestResults@2
      condition: succeededOrFailed()
      inputs:
        testResultsFiles: '**/test-*.xml'
        testRunTitle: 'Python $(pythonVersion)'
    
    - task: PublishCodeCoverageResults@1
      inputs:
        codeCoverageTool: Cobertura
        summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'

- stage: Quality
  dependsOn: Test
  jobs:
  - job: QualityGate
    steps:
    - script: |
        pip install pylint
        pylint src --exit-zero --output-format=json > pylint-results.json
      displayName: 'Run pylint'
    
    - task: PublishBuildArtifacts@1
      inputs:
        PathtoPublish: 'pylint-results.json'
        ArtifactName: 'quality-results'

- stage: Deploy
  dependsOn: Quality
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
  - deployment: Production
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - script: echo "Deploying to production"
"""

# ----------------------------------------------------------------------------
# [DOCKER] DOCKER POUR TESTS
# ----------------------------------------------------------------------------

"""
TESTS AVEC DOCKER
─────────────────

Environnement isolé et reproductible
"""

# Dockerfile.test
"""
FROM python:3.10-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["pytest", "--cov=src", "--cov-report=html"]
"""

# docker-compose.test.yml
"""
version: '3.8'

services:
  tests:
    build:
      context: .
      dockerfile: Dockerfile.test
    volumes:
      - .:/app
      - ./htmlcov:/app/htmlcov
    environment:
      - DATABASE_URL=postgresql://test:test@db:5432/testdb
    depends_on:
      - db
  
  db:
    image: postgres:14
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: testdb
    ports:
      - "5432:5432"
"""

# Lancer tests
"""
docker-compose -f docker-compose.test.yml up --abort-on-container-exit
"""

# ----------------------------------------------------------------------------
# [GRAPHIQUE] MONITORING QUALITÉ
# ----------------------------------------------------------------------------

"""
CODECOV
───────

Tracking coverage dans le temps
"""

# codecov.yml
"""
coverage:
  status:
    project:
      default:
        target: 80%
        threshold: 1%
    patch:
      default:
        target: 90%
  
  ignore:
    - "tests/"
    - "**/__init__.py"
"""

"""
SONARQUBE
─────────

Analyse qualité complète
"""

# sonar-project.properties
"""
sonar.projectKey=my-project
sonar.projectName=My Project
sonar.projectVersion=1.0

sonar.sources=src
sonar.tests=tests

sonar.python.coverage.reportPaths=coverage.xml
sonar.python.pylint.reportPath=pylint-report.txt

sonar.coverage.exclusions=**/*_test.py,**/test_*.py
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] STRATÉGIE DE BRANCHING
# ----------------------------------------------------------------------------

"""
GITFLOW + TDD
─────────────

main
  ├─ develop
  │   ├─ feature/user-auth (TDD ici)
  │   ├─ feature/payment
  │   └─ hotfix/bug-123

Règles :
1. Tests passent sur feature avant merge
2. Coverage maintenu sur develop
3. Quality gates sur PR vers develop
4. Tests complets avant merge main


TRUNK-BASED + TDD
─────────────────

main (toujours déployable)
  ├─ feature-flag-1
  ├─ feature-flag-2

Règles :
1. Commits fréquents sur main
2. Tests passent TOUJOURS
3. Feature flags pour code incomplet
4. Déploiement continu


[IDEE] BEST PRACTICES

[OK] Tests dans CI
   Toujours automatiques

[OK] Fail fast
   Arrêter dès premier échec

[OK] Feedback rapide
   Tests < 10 minutes

[OK] Coverage minimum
   80%+ enforced

[OK] Parallélisation
   Tests indépendants
"""

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] CI/CD définitions
   Integration, Delivery, Deployment

[OK] Pipelines
   GitHub Actions, GitLab, Azure

[OK] Quality gates
   Bloquer si tests échouent

[OK] Docker tests
   Environnement reproductible

[OK] Monitoring
   Codecov, SonarQube


[CLE] POINTS CLÉS

1. TDD + CI/CD = Puissant
   Feedback immédiat

2. Automatiser tout
   Tests, quality, deploy

3. Quality gates strictes
   Protection main branch

4. Tests rapides
   < 10 minutes idéalement

5. Monitoring continu
   Suivre qualité dans temps


[RAPIDE] PROCHAINE ÉTAPE

Chapitre 16 : Métriques et Coverage
Mesurer et interpréter
"""


# ============================================================================
# [GUIDE] CHAPITRE 16 : MÉTRIQUES ET COVERAGE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Mesurer coverage
[OK] Interpréter métriques
[OK] Mutation testing
[OK] Métriques qualité code
[OK] Éviter vanity metrics
"""

# ----------------------------------------------------------------------------
# [GRAPHIQUE] CODE COVERAGE
# ----------------------------------------------------------------------------

"""
TYPES DE COVERAGE
─────────────────

1. LINE COVERAGE (Couverture de lignes)
   % de lignes exécutées
"""

def divide(a, b):
    if b == 0:              # Ligne 1 : Exécutée
        raise ValueError()  # Ligne 2 : Pas exécutée si b != 0
    return a / b            # Ligne 3 : Exécutée

# Test
def test_divide():
    assert divide(10, 2) == 5

# Line coverage : 2/3 = 66%

"""
2. BRANCH COVERAGE (Couverture de branches)
   % de branches exécutées
"""

def absolute(x):
    if x < 0:       # Branch 1 : x < 0
        return -x
    return x        # Branch 2 : x >= 0

# Test
def test_absolute_positive():
    assert absolute(5) == 5

# Branch coverage : 1/2 = 50%

"""
3. FUNCTION COVERAGE
   % de fonctions appelées

4. STATEMENT COVERAGE
   % d'instructions exécutées


MESURER COVERAGE (Python)
──────────────────────────
"""

# Avec pytest
pytest --cov=src --cov-report=html --cov-report=term

# Output terminal
"""
Name                 Stmts   Miss  Cover
----------------------------------------
src/__init__.py          0      0   100%
src/calculator.py       20      2    90%
src/user.py             35      5    86%
----------------------------------------
TOTAL                   55      7    87%
"""

# Rapport HTML détaillé
# htmlcov/index.html

"""
CONFIGURER COVERAGE
───────────────────
"""

# .coveragerc
"""
[run]
source = src
omit = 
    */tests/*
    */__pycache__/*
    */venv/*

[report]
exclude_lines =
    pragma: no cover
    def __repr__
    raise AssertionError
    raise NotImplementedError
    if __name__ == .__main__.:

precision = 2

[html]
directory = htmlcov
"""

"""
COVERAGE MINIMUM
────────────────
"""

# Dans CI/CD
pytest --cov=src --cov-report=term --cov-fail-under=80

# Échoue si coverage < 80%

"""
[IDEE] QUEL SEUIL ?

Recommandations :
- Nouveau projet : 90%+
- Projet existant : 70%+
- Code critique : 95%+
- UI/Config : 50-60% OK

Mais ATTENTION aux vanity metrics !
"""

# ----------------------------------------------------------------------------
# [ATTENTION] LIMITES DU COVERAGE
# ----------------------------------------------------------------------------

"""
COVERAGE N'EST PAS QUALITÉ
──────────────────────────

[X] 100% coverage ≠ Code parfait
"""

# Exemple : 100% coverage mais test inutile
def add(a, b):
    return a + b

def test_add():
    add(2, 3)  # Pas d'assertion !
    # [OK] 100% coverage
    # [X] Test inutile

"""
FAUX SENTIMENT DE SÉCURITÉ
───────────────────────────
"""

def divide(a, b):
    return a / b  # Bug : pas de check b != 0

def test_divide():
    assert divide(10, 2) == 5
    # [OK] 100% coverage
    # [X] Bug non détecté (division par zéro)

"""
TEST QUALITÉ, PAS QUANTITÉ
───────────────────────────

[X] Mauvais test (100% coverage)
"""
def test_user_creation():
    user = create_user("Alice", "alice@example.com")
    assert user is not None  # Test faible !

"""
[OK] Bon test (100% coverage)
"""
def test_user_creation():
    user = create_user("Alice", "alice@example.com")
    assert user.name == "Alice"
    assert user.email == "alice@example.com"
    assert user.is_active is True
    assert user.id is not None

"""
[IDEE] RÈGLES D'OR

1. Coverage = Métrique utile mais insuffisante
2. Qualité assertions > Coverage %
3. Tester comportement, pas lignes
4. Coverage guide, pas objectif
5. 80% avec bons tests > 100% avec mauvais tests
"""

# ----------------------------------------------------------------------------
# [SCIENCE] MUTATION TESTING
# ----------------------------------------------------------------------------

"""
MUTATION TESTING
────────────────

Définition : Introduire bugs intentionnels (mutations)
Vérifier si tests les détectent


COMMENT ÇA MARCHE ?
───────────────────

1. Code original
"""
def is_adult(age):
    return age >= 18

"""
2. Mutations possibles
"""
# Mutation 1 : Changer opérateur
def is_adult(age):
    return age > 18  # >= devient >

# Mutation 2 : Changer constante
def is_adult(age):
    return age >= 19  # 18 devient 19

# Mutation 3 : Inverser condition
def is_adult(age):
    return age < 18  # >= devient <

"""
3. Lancer tests sur chaque mutation
   Si tests passent -> Mutation "survit" -> Tests insuffisants !
   Si tests échouent -> Mutation "tuée" -> Tests bons !


SCORE MUTATION
──────────────

Mutation Score = (Mutants tués / Total mutants) × 100%

Bon score : 80%+


OUTIL : mutmut (Python)
────────────────────────
"""

# Installation
pip install mutmut

# Lancer mutation testing
mutmut run

# Output
"""
- Mutation testing starting -

These are the steps:
1. A full test suite run will be made to make sure we
   can run the tests successfully and we know how long
   it takes (to detect infinite loops for example)
2. Mutants will be generated and checked

Killed mutants: 45
Survived mutants: 3
Timeout mutants: 0
Mutation score: 93.75%
"""

# Voir mutants survivants
mutmut results

# Voir détails
mutmut show 1

"""
EXEMPLE CONCRET
───────────────

Code :
"""
def calculate_discount(price, is_vip):
    if is_vip:
        return price * 0.8
    return price

"""
Test insuffisant :
"""
def test_calculate_discount():
    result = calculate_discount(100, True)
    assert result == 80
    # [X] Pas de test pour is_vip=False

"""
Mutation qui survit :
"""
def calculate_discount(price, is_vip):
    if is_vip:
        return price * 0.8
    return price * 1.1  # Mutation : return price -> return price * 1.1

# Test passe quand même (car seul cas is_vip=True testé)

"""
Améliorer tests :
"""
def test_calculate_discount_vip():
    assert calculate_discount(100, True) == 80

def test_calculate_discount_normal():
    assert calculate_discount(100, False) == 100

# Maintenant mutation sera détectée !

"""
[IDEE] MUTATION TESTING = Tester les tests !

Révèle faiblesses des tests
Plus strict que coverage
Score 80%+ = Tests robustes
"""

# ----------------------------------------------------------------------------
# [HAUSSE] AUTRES MÉTRIQUES QUALITÉ
# ----------------------------------------------------------------------------

"""
COMPLEXITÉ CYCLOMATIQUE
───────────────────────

Mesure : Nombre de chemins indépendants dans le code

Formule : M = E - N + 2P
  E = Edges (arêtes)
  N = Nodes (nœuds)
  P = Composants connectés
"""

# Exemple
def calculate_grade(score):
    if score >= 90:
        return 'A'
    elif score >= 80:
        return 'B'
    elif score >= 70:
        return 'C'
    elif score >= 60:
        return 'D'
    else:
        return 'F'

# Complexité : 5 (5 chemins possibles)

"""
Interprétation :
1-10  : Simple, peu de risque
11-20 : Complexe, risque moyen
21-50 : Très complexe, risque élevé
50+   : Non testable, refactorer !


Mesurer avec radon (Python) :
"""
pip install radon
radon cc src/ -a

# Output
"""
src/calculator.py
    M 45:0 calculate_grade - B (5)
    M 60:0 process_order - C (12)

Average complexity: B (8.5)
"""

"""
MAINTENABILITÉ INDEX
────────────────────

Score : 0-100
100 = Parfait
<20 = Difficile à maintenir
"""

radon mi src/

# Output
"""
src/calculator.py - A (85.2)
src/user.py - B (72.1)
src/legacy.py - C (45.3)
"""

"""
DEBT TECHNIQUE
──────────────

SonarQube calcule "dette technique"
Temps estimé pour corriger tous les problèmes
"""

# Exemple rapport SonarQube
"""
Technical Debt: 3 days 4 hours
Debt Ratio: 8.5%
Bugs: 12
Code Smells: 45
Vulnerabilities: 3
"""

"""
MÉTRIQUES TDD
─────────────

1. Temps cycle Red-Green-Refactor
   Objectif : < 5 minutes

2. Ratio Tests/Code
   1:1 à 2:1 typique

3. Vitesse tests
   Unitaires : < 100ms chacun
   Suite complète : < 10 min

4. Fréquence commits
   Plusieurs fois par jour (TDD)
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] DASHBOARD QUALITÉ
# ----------------------------------------------------------------------------

"""
BADGE README
────────────
"""

# README.md
"""
# My Project

![Tests](https://github.com/user/repo/workflows/Tests/badge.svg)
![Coverage](https://codecov.io/gh/user/repo/branch/main/graph/badge.svg)
![Quality](https://sonarcloud.io/api/project_badges/measure?project=key&metric=alert_status)

[![Maintainability](https://api.codeclimate.com/v1/badges/abc/maintainability)](https://codeclimate.com/github/user/repo)
"""

"""
GRAFANA DASHBOARD
─────────────────

Suivre métriques dans le temps :
- Coverage trend
- Test execution time
- Mutation score
- Debt technique
- Nombre bugs
"""

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Code coverage
   Line, branch, function

[OK] Limites coverage
   Pas garantie qualité

[OK] Mutation testing
   Tester les tests

[OK] Métriques qualité
   Complexité, maintenabilité

[OK] Dashboard
   Suivre qualité


[CLE] POINTS CLÉS

1. Coverage = Guide
   Pas objectif absolu

2. Qualité > Quantité
   Bons tests > % élevé

3. Mutation testing = Robustesse
   Révèle faiblesses

4. Complexité = Testabilité
   Simple = Testable

5. Monitoring continu
   Tendances importantes


[THOUGHT_BALLOON] CITATION

"If you can't measure it, you can't improve it."
- Peter Drucker


[RAPIDE] PROCHAINE ÉTAPE

Chapitre 17 : Best Practices Avancées
Synthèse finale
"""


# ============================================================================
# [GUIDE] CHAPITRE 17 : BEST PRACTICES AVANCÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Best practices TDD avancées
[OK] Gérer équipe TDD
[OK] TDD à grande échelle
[OK] Pièges à éviter
[OK] Continuer à progresser
"""

# ----------------------------------------------------------------------------
# [TROPHEE] BEST PRACTICES MASTER
# ----------------------------------------------------------------------------

"""
1. TEST NOMMÉ CLAIREMENT
────────────────────────
"""

# [X] Mauvais nom
def test_user_1():
    pass

# [OK] Bon nom
def test_create_user_with_valid_email_saves_to_database():
    pass

# [OK] Encore mieux (Given-When-Then)
def test_given_valid_email_when_creating_user_then_saves_to_database():
    pass

"""
2. UN TEST = UN CONCEPT
───────────────────────
"""

# [X] Test fait trop de choses
def test_user():
    user = create_user("Alice")
    assert user.name == "Alice"
    
    user.update_email("new@example.com")
    assert user.email == "new@example.com"
    
    user.deactivate()
    assert user.is_active is False

# [OK] Tests séparés
def test_create_user_sets_name():
    user = create_user("Alice")
    assert user.name == "Alice"

def test_update_email_changes_email():
    user = create_user("Alice")
    user.update_email("new@example.com")
    assert user.email == "new@example.com"

def test_deactivate_sets_inactive():
    user = create_user("Alice")
    user.deactivate()
    assert user.is_active is False

"""
3. AAA PATTERN TOUJOURS
───────────────────────
"""

def test_calculate_total():
    # ARRANGE (Given)
    cart = ShoppingCart()
    cart.add_item(Item("Book", 10), quantity=2)
    
    # ACT (When)
    total = cart.calculate_total()
    
    # ASSERT (Then)
    assert total == 20

"""
4. FIXTURES RÉUTILISABLES
─────────────────────────
"""

@pytest.fixture
def sample_cart():
    """Fixture cart pré-rempli"""
    cart = ShoppingCart()
    cart.add_item(Item("Book", 10), 2)
    cart.add_item(Item("Pen", 1), 5)
    return cart

def test_total_with_fixture(sample_cart):
    assert sample_cart.calculate_total() == 25

def test_item_count_with_fixture(sample_cart):
    assert len(sample_cart.items) == 2

"""
5. TESTS INDÉPENDANTS
─────────────────────

[X] Tests dépendants
"""
user_id = None

def test_1_create_user():
    global user_id
    user_id = create_user("Alice")

def test_2_update_user():
    # Dépend de test_1 !
    update_user(user_id, email="new@example.com")

"""
[OK] Tests indépendants
"""
def test_create_user():
    user_id = create_user("Alice")
    # Test complet

def test_update_user():
    user_id = create_user("Bob")  # Setup propre
    update_user(user_id, email="new@example.com")
    # Test complet

"""
6. TESTS RAPIDES
────────────────

Optimisations :
"""

# [OK] Mock I/O
@patch('requests.get')
def test_api_call(mock_get):
    mock_get.return_value.json.return_value = {"data": "test"}
    # Pas d'appel réseau réel

# [OK] DB en mémoire
@pytest.fixture
def db():
    return SQLiteDatabase(':memory:')

# [OK] Parallélisation
pytest -n auto  # Parallèle automatique

"""
7. TESTS DÉTERMINISTES
──────────────────────
"""

# [X] Test flaky
def test_timestamp():
    user = create_user("Alice")
    assert user.created_at == datetime.now()  # Peut échouer !

# [OK] Test stable
def test_timestamp():
    now = datetime.now()
    user = create_user("Alice", created_at=now)
    assert user.created_at == now

# [OK] Ou mock time
with freeze_time("2024-01-01 12:00:00"):
    user = create_user("Alice")
    assert user.created_at == datetime(2024, 1, 1, 12, 0, 0)

"""
8. ÉVITER LOGIQUE DANS TESTS
─────────────────────────────
"""

# [X] Logique dans test
def test_bad():
    users = get_users()
    for user in users:
        if user.is_active:
            assert user.email is not None

# [OK] Test simple et direct
def test_good():
    user = User(is_active=True)
    assert user.email is not None

"""
9. MESSAGES D'ERREUR CLAIRS
────────────────────────────
"""

# [X] Message peu clair
assert result == expected

# [OK] Message descriptif
assert result == expected, \
    f"Expected discount to be {expected}, got {result}"

# [OK] Ou pytest automatique (déjà bon)
assert result == expected  # pytest montre les deux valeurs

"""
10. REFACTORING TESTS AUSSI
────────────────────────────

Tests = Code
Donc refactorer régulièrement
"""

# Avant : Duplication
def test_user_alice():
    db = Database()
    service = UserService(db)
    user = service.create("Alice")
    assert user.name == "Alice"

def test_user_bob():
    db = Database()
    service = UserService(db)
    user = service.create("Bob")
    assert user.name == "Bob"

# Après : Fixture
@pytest.fixture
def user_service():
    db = Database()
    return UserService(db)

def test_user_alice(user_service):
    user = user_service.create("Alice")
    assert user.name == "Alice"

def test_user_bob(user_service):
    user = user_service.create("Bob")
    assert user.name == "Bob"

# ----------------------------------------------------------------------------
# [UTILISATEURS] GÉRER ÉQUIPE TDD
# ----------------------------------------------------------------------------

"""
ADOPTER TDD EN ÉQUIPE
─────────────────────

1. FORMATION
   - Katas en groupe
   - Pair programming
   - Workshops

2. CODE REVIEW
   - Vérifier tests
   - Tests = First-class citizens
   - Refuser PR sans tests

3. MÉTRIQUES PARTAGÉES
   - Coverage visible
   - Dashboard équipe
   - Célébrer succès

4. TEMPS DÉDIÉ
   - TDD n'est pas "extra"
   - Inclus dans estimations
   - Refactoring planifié


PAIR PROGRAMMING
────────────────

Ping-Pong TDD :

1. Alice écrit test (RED)
2. Bob écrit code (GREEN)
3. Alice refactore (REFACTOR)
4. Bob écrit nouveau test (RED)
5. Répéter...

Bénéfices :
[OK] Partage connaissances
[OK] Qualité supérieure
[OK] Moins de bugs
[OK] Code review intégré


MOB PROGRAMMING
───────────────

Équipe entière sur un problème :
- 1 driver (clavier)
- N navigateurs (guident)
- Rotation toutes les 5-10 min

TDD en mob :
- Équipe décide prochain test
- Driver écrit code
- Qualité maximale
"""

# ----------------------------------------------------------------------------
# [ENTREPRISE] TDD À GRANDE ÉCHELLE
# ----------------------------------------------------------------------------

"""
MICROSERVICES + TDD
───────────────────

Chaque service :
- Tests unitaires (rapides)
- Tests d'intégration (DB, cache)
- Tests contrat (API)

Entre services :
- Contract testing (Pact)
- Tests E2E (peu nombreux)


MONOREPO + TDD
──────────────

Structure :
"""
repo/
├── services/
│   ├── user-service/
│   │   ├── src/
│   │   └── tests/
│   ├── order-service/
│   │   ├── src/
│   │   └── tests/
├── shared/
│   ├── models/
│   └── utils/

"""
CI optimisé :
- Détecter services modifiés
- Lancer seulement tests affectés
- Paralléliser par service


LEGACY CODEBASE
───────────────

Stratégie progressive :

Phase 1 : Nouveau code en TDD (2-3 mois)
Phase 2 : Tests pour bugs fixés (ongoing)
Phase 3 : Refactoring progressif (6-12 mois)
Phase 4 : Coverage 80%+ (1-2 ans)

Ne PAS essayer 100% tests en une fois !
"""

# ----------------------------------------------------------------------------
# [ATTENTION] PIÈGES AVANCÉS
# ----------------------------------------------------------------------------

"""
PIÈGE 1 : OVER-ENGINEERING
──────────────────────────

[X] Trop de couches
"""
class UserController:
    def __init__(self, facade):
        self.facade = facade

class UserFacade:
    def __init__(self, service):
        self.service = service

class UserService:
    def __init__(self, manager):
        self.manager = manager

class UserManager:
    def __init__(self, repository):
        self.repository = repository

# 5 couches pour créer un user !

"""
[OK] Simple et testable
"""
class UserService:
    def __init__(self, repository):
        self.repository = repository
    
    def create_user(self, data):
        user = User(**data)
        return self.repository.save(user)

"""
PIÈGE 2 : TESTS COUPLÉS À L'IMPLÉMENTATION
───────────────────────────────────────────

[X] Test connaît trop de détails internes
"""
def test_user_creation():
    service = UserService()
    service._validate(data)  # [X] Méthode privée
    service._hash_password(password)  # [X] Détail interne
    service._save_to_db(user)  # [X] Implémentation
    # Refactoring cassera test !

"""
[OK] Test comportement public
"""
def test_user_creation():
    service = UserService()
    user = service.create_user("Alice", "alice@example.com")
    assert user.name == "Alice"
    # Refactoring interne OK !

"""
PIÈGE 3 : OUBLIER REFACTORING
──────────────────────────────

TDD = Red -> Green -> REFACTOR

Pas Red -> Green -> Prochain test

Refactoring n'est pas optionnel !


PIÈGE 4 : TESTS TROP LENTS
───────────────────────────

Si tests > 10 minutes :
- Développeurs ne les lancent pas
- Cycle TDD cassé
- Feedback trop lent

Solutions :
[OK] Tests parallèles
[OK] Tests sélectifs (changed files)
[OK] Mock I/O
[OK] DB en mémoire
"""

# ----------------------------------------------------------------------------
# [COURS] CONTINUER À PROGRESSER
# ----------------------------------------------------------------------------

"""
KATAS RECOMMANDÉS
─────────────────

Débutant :
- FizzBuzz
- String Calculator
- Bowling Game
- Prime Factors

Intermédiaire :
- Roman Numerals
- Tennis Game
- Mars Rover
- Gilded Rose

Avancé :
- Banking Kata
- Trip Service
- Theatrical Players (Refactoring)


RESSOURCES
──────────

Livres :
[DOCS] "Test Driven Development" - Kent Beck
[DOCS] "Growing Object-Oriented Software, Guided by Tests" - Freeman & Pryce
[DOCS] "Working Effectively with Legacy Code" - Michael Feathers
[DOCS] "Refactoring" - Martin Fowler

Sites :
[WEB] exercism.io (exercices guidés)
[WEB] codewars.com (katas)
[WEB] cyber-dojo.org (TDD online)


COMMUNAUTÉ
──────────

[MOVIE_CAMERA] Conférences :
- Agile Alliance
- XP Days
- Craft Conf

[UTILISATEURS] Meetups :
- Software Craftsmanship
- Coding Dojos locaux

[SPEECH_BALLOON] Forums :
- Stack Overflow [tdd] tag
- Reddit r/tdd
- Twitter #TDD


PRATIQUER RÉGULIÈREMENT
───────────────────────

[IDEE] Suggestion :

1 kata par semaine = Maîtrise en 6 mois
Pair programming 2x/semaine
Code review focus tests
Refactoring vendredi après-midi
"""

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

"""
CE QUE VOUS AVEZ APPRIS

[OK] Best practices master
   10 règles essentielles

[OK] Gérer équipe TDD
   Formation, pair programming

[OK] TDD à grande échelle
   Microservices, legacy

[OK] Pièges avancés
   Over-engineering, couplage

[OK] Continuer à progresser
   Katas, ressources, communauté


[CLE] POINTS CLÉS FINAUX

1. TDD = Discipline
   Pratiquer régulièrement

2. Tests = Code
   Même soin et qualité

3. Simple > Complexe
   KISS, YAGNI

4. Équipe > Individu
   Pair programming, partage

5. Amélioration continue
   Toujours apprendre


[THOUGHT_BALLOON] CITATIONS FINALES

"Any fool can write code that a computer can understand.
Good programmers write code that humans can understand."
- Martin Fowler

"The only way to go fast, is to go well."
- Robert C. Martin

"Make it work, make it right, make it fast."
- Kent Beck


═══════════════════════════════════════════════════════════════
[BRAVO] FÉLICITATIONS ! VOUS AVEZ TERMINÉ LE GUIDE TDD COMPLET ! [BRAVO]
═══════════════════════════════════════════════════════════════

Vous maîtrisez maintenant :
[OK] Philosophie TDD (Partie 0)
[OK] Fondamentaux pratiques (Partie 1)
[OK] Pratique avancée (Partie 2)
[OK] Maîtrise complète (Partie 3)

17 chapitres
100+ exemples concrets
Tous les langages principaux
De débutant à expert

VOUS ÊTES MAINTENANT UN EXPERT TDD ! [RAPIDE]

Continuez à pratiquer
Partagez vos connaissances
Construisez du code de qualité

Bonne chance dans votre parcours TDD ! [FORCE]
═══════════════════════════════════════════════════════════════
"""

# ============================================================================
# FIN DU GUIDE TDD COMPLET
# ============================================================================