# ============================================================================
# [LIVRE] PYTEST - GUIDE ULTRA-DÉTAILLÉ POUR GRANDS DÉBUTANTS - PARTIE 1
# ============================================================================
# 
# [OBJECTIF] OBJECTIF DE CE GUIDE
# Ce guide est conçu pour quelqu'un qui n'a JAMAIS fait de tests automatisés.
# Chaque concept est expliqué avec:
# - POURQUOI il existe (le problème qu'il résout)
# - QUAND l'utiliser (dans quelles situations)
# - COMMENT l'implémenter (avec exemples progressifs)
#
# [DOCS] PRÉREQUIS
# - Python de base (variables, fonctions, classes)
# - Comprendre ce qu'est un bug
# - Avoir installé Python 3.7+
#
# [TEMPS] TEMPS DE LECTURE: ~3-4 heures par partie
# Prenez votre temps, testez chaque exemple!
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 0: COMPRENDRE LES TESTS (AVANT PYTEST)
# ============================================================================

"""
[REFLEXION] POURQUOI TESTER SON CODE?

Pour comprendre pytest, commençons par comprendre pourquoi on teste.

SCÉNARIO SANS TESTS:
-------------------
"""

# calculator.py
def add(a, b):
    """Additionner deux nombres"""
    return a + b

def divide(a, b):
    """Diviser deux nombres"""
    return a / b

"""
Vous écrivez ce code. Comment savoir s'il fonctionne?

MÉTHODE 1: TESTER MANUELLEMENT (mauvais!)
"""

# Test manuel dans le terminal
print(add(2, 3))      # 5 - OK
print(add(-1, 1))     # 0 - OK
print(divide(10, 2))  # 5.0 - OK
print(divide(10, 0))  # [IMPACT] CRASH! ZeroDivisionError

"""
[X] PROBLÈMES DU TEST MANUEL:

1. RÉPÉTITIF:
   - Chaque modification -> retester TOUT à la main
   - 10 fonctions -> 10 tests manuels
   - 100 fonctions -> ... impossible!

2. OUBLIS:
   - Vous oubliez de tester divide(10, 0)
   - Le bug arrive en production
   - Les utilisateurs sont mécontents

3. PAS DE PREUVE:
   - "Ça marchait sur ma machine!"
   - Impossible de prouver que ça fonctionne

4. PEUR DE MODIFIER:
   - Si je change add(), est-ce que je casse quelque chose?
   - Résultat: code qui vieillit mal


MÉTHODE 2: TESTS AUTOMATISÉS (bon!)
"""

# test_calculator.py
def test_add_positive_numbers():
    """Test: additionner deux nombres positifs"""
    result = add(2, 3)
    assert result == 5, f"Expected 5, got {result}"

def test_add_negative_numbers():
    """Test: additionner avec négatifs"""
    result = add(-1, 1)
    assert result == 0, f"Expected 0, got {result}"

def test_divide_normal():
    """Test: division normale"""
    result = divide(10, 2)
    assert result == 5.0, f"Expected 5.0, got {result}"

def test_divide_by_zero():
    """Test: division par zéro doit échouer"""
    try:
        divide(10, 0)
        assert False, "Should have raised ZeroDivisionError"
    except ZeroDivisionError:
        pass  # C'est ce qu'on attend!

"""
[OK] AVANTAGES DES TESTS AUTOMATISÉS:

1. RAPIDE:
   - 1 commande -> tous les tests s'exécutent
   - pytest test_calculator.py
   - Résultat en quelques secondes

2. FIABLE:
   - Les tests ne "oublient" pas
   - Toujours les mêmes vérifications

3. DOCUMENTATION VIVANTE:
   - Les tests montrent COMMENT utiliser le code
   - Exemples concrets

4. CONFIANCE:
   - Vous pouvez modifier le code
   - Les tests vous disent si vous cassez quelque chose


[IDEE] ANALOGIE SIMPLE:

Imaginez que vous cuisinez une recette:

SANS TESTS:
- Vous goûtez avec votre doigt
- Vous devinez si c'est bon
- Vous espérez que ça marchera

AVEC TESTS:
- Vous avez un thermomètre (température exacte)
- Vous avez une balance (quantités précises)
- Vous êtes SÛR du résultat


[OBJECTIF] QU'EST-CE QU'UN TEST?

Un test = Une fonction qui VÉRIFIE que votre code fonctionne

STRUCTURE D'UN TEST:
"""

def test_something():
    # 1. ARRANGE: Préparer les données
    input_value = 5
    
    # 2. ACT: Exécuter l'action à tester
    result = my_function(input_value)
    
    # 3. ASSERT: Vérifier le résultat
    assert result == expected_value

"""
Les 3 A du testing:
- ARRANGE: Mise en place
- ACT: Action
- ASSERT: Vérification


[GRAPHIQUE] TYPES DE TESTS:

1. TESTS UNITAIRES (Unit Tests):
   - Testent UNE fonction/méthode isolée
   - Rapides (millisecondes)
   - Nombreux (des centaines)
   - 70% de vos tests

2. TESTS D'INTÉGRATION (Integration Tests):
   - Testent plusieurs composants ensemble
   - Plus lents (secondes)
   - Moins nombreux
   - 20% de vos tests

3. TESTS END-TO-END (E2E Tests):
   - Testent l'application complète
   - Lents (minutes)
   - Peu nombreux
   - 10% de vos tests

PYRAMIDE DES TESTS:
        /\
       /E2E\      <- Peu, lents, fragiles
      /------\
     /  INTG  \   <- Moyens
    /----------\
   /   UNIT     \  <- Nombreux, rapides, solides
  /--------------\


[COURS] PYTEST VS AUTRES FRAMEWORKS:

PYTHON A PLUSIEURS FRAMEWORKS DE TEST:

1. unittest (intégré à Python):
   - Verbeux (beaucoup de code)
   - Style orienté objet
   - assert devient assertEqual(), assertTrue(), etc.

2. pytest (RECOMMANDÉ):
   - Simple et pythonique
   - assert normal de Python
   - Très puissant
   - Plugins nombreux

COMPARAISON:
"""

# === AVEC unittest (verbeux) ===
import unittest

class TestCalculator(unittest.TestCase):
    def test_add(self):
        result = add(2, 3)
        self.assertEqual(result, 5)
    
    def test_divide_by_zero(self):
        with self.assertRaises(ZeroDivisionError):
            divide(10, 0)

if __name__ == '__main__':
    unittest.main()


# === AVEC pytest (simple) ===
def test_add():
    assert add(2, 3) == 5

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

# C'est tout! Pas de classe, pas de self.assertEqual()

"""
[IDEE] POURQUOI PYTEST?

[OK] Simple: assert normal de Python
[OK] Puissant: fixtures, parametrize, plugins
[OK] Rapide: exécution parallèle
[OK] Informatif: messages d'erreur détaillés
[OK] Flexible: fonctionne avec n'importe quel code Python


[OBJECTIF] CONCEPT CLÉ À RETENIR:

Tests = SPÉCIFICATIONS exécutables de votre code

Au lieu de documenter "add() doit retourner la somme de a et b",
vous MONTREZ que add(2, 3) == 5

Les tests sont votre FILET DE SÉCURITÉ!
"""


# ============================================================================
# [GUIDE] CHAPITRE 1: INSTALLATION ET PREMIER TEST
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Installer pytest et écrire votre tout premier test qui fonctionne.


ÉTAPE 1: INSTALLATION
--------------------
"""

# Installation simple
pip install pytest

# Vérifier l'installation
pytest --version

"""
Vous devriez voir quelque chose comme:
pytest 7.4.0


ÉTAPE 2: STRUCTURE DE PROJET
---------------------------

Créons une structure propre dès le début:
"""

my_project/
├── src/
│   ├── __init__.py
│   └── calculator.py       # Votre code
├── tests/
│   ├── __init__.py
│   └── test_calculator.py  # Vos tests
└── pytest.ini              # Configuration (optionnel)

"""
[IDEE] CONVENTIONS IMPORTANTES:

1. NOMS DE FICHIERS:
   - Tests: test_*.py ou *_test.py
   - pytest cherche automatiquement ces fichiers

2. NOMS DE FONCTIONS:
   - Tests: test_*
   - pytest cherche automatiquement ces fonctions

3. NOMS DE CLASSES (optionnel):
   - Tests: Test*
   - Si vous organisez en classes


[ATTENTION] ERREUR COURANTE:

Fichier nommé: calculator_test.py [X] (ne fonctionne pas par défaut)
Fichier nommé: test_calculator.py [OK]
Fichier nommé: calculator_tests.py [OK] (avec pytest.ini modifié)


ÉTAPE 3: PREMIER CODE À TESTER
-----------------------------
"""

# src/calculator.py
"""Module calculator: opérations mathématiques simples"""

def add(a, b):
    """
    Additionne deux nombres.
    
    Args:
        a: Premier nombre
        b: Deuxième nombre
    
    Returns:
        La somme de a et b
    
    Examples:
        >>> add(2, 3)
        5
        >>> add(-1, 1)
        0
    """
    return a + b


def subtract(a, b):
    """Soustrait b de a"""
    return a - b


def multiply(a, b):
    """Multiplie deux nombres"""
    return a * b


def divide(a, b):
    """
    Divise a par b.
    
    Args:
        a: Numérateur
        b: Dénominateur
    
    Returns:
        Le quotient de a / b
    
    Raises:
        ZeroDivisionError: Si b est zéro
    """
    if b == 0:
        raise ZeroDivisionError("Cannot divide by zero")
    return a / b

"""
[IDEE] POINTS À NOTER:

1. Docstrings claires
2. Gestion explicite des erreurs
3. Code simple (facile à tester)


ÉTAPE 4: PREMIER TEST
--------------------
"""

# tests/test_calculator.py
"""Tests pour le module calculator"""

from src.calculator import add, subtract, multiply, divide


def test_add_positive_numbers():
    """
    [IDEE] VOTRE PREMIER TEST!
    
    Teste que add() fonctionne avec des nombres positifs.
    
    STRUCTURE:
    1. ARRANGE: Préparer les données
    2. ACT: Appeler la fonction
    3. ASSERT: Vérifier le résultat
    """
    # ARRANGE: Préparer
    a = 2
    b = 3
    expected = 5
    
    # ACT: Exécuter
    result = add(a, b)
    
    # ASSERT: Vérifier
    assert result == expected

"""
[REFLEXION] DÉCORTIQUONS CE TEST:

def test_add_positive_numbers():
    ^^^^^^ Doit commencer par "test_"
    
    Pytest découvre automatiquement cette fonction
    parce qu'elle commence par "test_".

assert result == expected
^^^^^^ Mot-clé Python standard!

Si result != expected:
- Le test ÉCHOUE
- Pytest affiche une erreur détaillée

Si result == expected:
- Le test RÉUSSIT
- Pytest continue avec le test suivant


ÉTAPE 5: LANCER LES TESTS
------------------------
"""

# Dans le terminal, à la racine du projet:
pytest

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

1. pytest cherche tous les fichiers test_*.py
2. Dans chaque fichier, cherche les fonctions test_*
3. Exécute chaque fonction
4. Affiche les résultats


RÉSULTAT SI SUCCÈS:
"""
============================= test session starts ==============================
platform linux -- Python 3.11.0, pytest-7.4.0
rootdir: /home/user/my_project
collected 1 item

tests/test_calculator.py .                                               [100%]

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

"""
[IDEE] COMPRENDRE L'OUTPUT:

collected 1 item
    -> pytest a trouvé 1 test

tests/test_calculator.py .
    -> Le point (.) = test réussi
    -> Si échec: F
    -> Si erreur: E
    -> Si skip: s

1 passed in 0.01s
    -> 1 test passé en 0.01 secondes


RÉSULTAT SI ÉCHEC:
"""

# Modifions le test pour qu'il échoue:
def test_add_positive_numbers():
    assert add(2, 3) == 6  # Faux! 2+3=5, pas 6

"""
Output:
"""
============================= test session starts ==============================
collected 1 item

tests/test_calculator.py F                                               [100%]

=================================== FAILURES ===================================
_________________________ test_add_positive_numbers ____________________________

    def test_add_positive_numbers():
>       assert add(2, 3) == 6
E       assert 5 == 6
E        +  where 5 = add(2, 3)

tests/test_calculator.py:8: AssertionError
=========================== short test summary info ============================
FAILED tests/test_calculator.py::test_add_positive_numbers - assert 5 == 6
============================== 1 failed in 0.03s ===============================

"""
[IDEE] COMPRENDRE L'ERREUR:

F -> Test échoué (Failure)

>       assert add(2, 3) == 6
    -> Ligne qui a échoué

E       assert 5 == 6
    -> Ce que pytest a trouvé

E        +  where 5 = add(2, 3)
    -> D'où vient la valeur 5

[IMPACT] PYTEST EST INTELLIGENT!
Il vous montre EXACTEMENT ce qui ne va pas:
- Valeur attendue: 6
- Valeur obtenue: 5
- Appel de fonction: add(2, 3)


ÉTAPE 6: TESTS SUPPLÉMENTAIRES
-----------------------------
"""

# tests/test_calculator.py
def test_add_positive_numbers():
    """Test avec nombres positifs"""
    assert add(2, 3) == 5


def test_add_negative_numbers():
    """
    [IDEE] DEUXIÈME TEST
    
    Teste avec des nombres négatifs.
    C'est important de tester différents cas!
    """
    assert add(-1, 1) == 0
    assert add(-5, -3) == -8


def test_add_zero():
    """
    [IDEE] EDGE CASE (cas limite)
    
    Toujours tester les cas limites:
    - Zéro
    - Nombres négatifs
    - Très grands nombres
    - Valeurs nulles
    """
    assert add(5, 0) == 5
    assert add(0, 0) == 0


def test_subtract():
    """Test de soustraction"""
    assert subtract(10, 3) == 7
    assert subtract(5, 10) == -5


def test_multiply():
    """Test de multiplication"""
    assert multiply(3, 4) == 12
    assert multiply(-2, 3) == -6
    assert multiply(0, 100) == 0


def test_divide_normal():
    """Test de division normale"""
    assert divide(10, 2) == 5.0
    assert divide(7, 2) == 3.5


def test_divide_by_zero():
    """
    [IDEE] TESTER LES EXCEPTIONS
    
    Comment tester qu'une fonction DOIT échouer?
    Utilisez pytest.raises()
    """
    import pytest
    
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)
    
    # Si divide(10, 0) NE lève PAS d'exception:
    # -> Le test ÉCHOUE
    
    # Si divide(10, 0) lève ZeroDivisionError:
    # -> Le test RÉUSSIT

"""
[IDEE] PYTEST.RAISES() EXPLIQUÉ:

with pytest.raises(ExceptionType):
    code_qui_doit_lever_exception()

Comme un try/except inversé:
- Si exception levée -> test réussit [OK]
- Si pas d'exception -> test échoue [X]


LANCER LES TESTS:
"""
pytest

"""
Output:
"""
collected 7 items

tests/test_calculator.py .......                                        [100%]

============================== 7 passed in 0.02s ===============================

"""
7 points (.) = 7 tests réussis!


[COURS] OPTIONS DE LIGNE DE COMMANDE UTILES
-------------------------------------
"""

# Verbose (plus de détails)
pytest -v

"""
Output:
"""
tests/test_calculator.py::test_add_positive_numbers PASSED           [ 14%]
tests/test_calculator.py::test_add_negative_numbers PASSED           [ 28%]
tests/test_calculator.py::test_add_zero PASSED                       [ 42%]
tests/test_calculator.py::test_subtract PASSED                       [ 57%]
tests/test_calculator.py::test_multiply PASSED                       [ 71%]
tests/test_calculator.py::test_divide_normal PASSED                  [ 85%]
tests/test_calculator.py::test_divide_by_zero PASSED                 [100%]

"""
Maintenant vous voyez le NOM de chaque test!


# Très verbose (encore plus de détails)
pytest -vv

# Arrêter au premier échec
pytest -x

# Afficher print() dans les tests
pytest -s

# Lancer seulement certains tests
pytest tests/test_calculator.py::test_add_positive_numbers

# Pattern de nom
pytest -k "add"  # Seulement tests avec "add" dans le nom

# Afficher les temps d'exécution
pytest --durations=10  # Les 10 tests les plus lents


[DOSSIER] ORGANISATION DES TESTS
------------------------

Au fur et à mesure que votre projet grandit:
"""

tests/
├── __init__.py
├── conftest.py              # Configuration partagée (on verra plus tard)
├── test_calculator.py       # Tests pour calculator.py
├── test_string_utils.py     # Tests pour string_utils.py
├── unit/                    # Tests unitaires
│   ├── test_models.py
│   └── test_utils.py
└── integration/             # Tests d'intégration
    ├── test_api.py
    └── test_database.py

"""
Lancez tous les tests:
"""
pytest

"""
Ou un dossier spécifique:
"""
pytest tests/unit/


[COURS] EXERCICE PRATIQUE #1: PREMIERS TESTS
--------------------------------------

Créez un fichier string_utils.py:
"""

# src/string_utils.py
def reverse_string(s):
    """Inverse une chaîne"""
    return s[::-1]

def count_vowels(s):
    """Compte les voyelles dans une chaîne"""
    vowels = "aeiouAEIOU"
    return sum(1 for char in s if char in vowels)

def is_palindrome(s):
    """Vérifie si une chaîne est un palindrome"""
    s_clean = s.lower().replace(" ", "")
    return s_clean == s_clean[::-1]

"""
MISSION: Écrivez des tests pour ces fonctions!

Créez tests/test_string_utils.py et testez:

1. reverse_string:
   - Chaîne normale: "hello" -> "olleh"
   - Chaîne vide: "" -> ""
   - Un seul caractère: "a" -> "a"

2. count_vowels:
   - "hello" -> 2 (e, o)
   - "aeiou" -> 5
   - "bcdfg" -> 0
   - "HELLO" -> 2 (majuscules aussi)

3. is_palindrome:
   - "radar" -> True
   - "hello" -> False
   - "A man a plan a canal Panama" -> True


SOLUTION:
"""

# tests/test_string_utils.py
from src.string_utils import reverse_string, count_vowels, is_palindrome


def test_reverse_string_normal():
    """Test reverse avec chaîne normale"""
    assert reverse_string("hello") == "olleh"


def test_reverse_string_empty():
    """Test reverse avec chaîne vide"""
    assert reverse_string("") == ""


def test_reverse_string_single_char():
    """Test reverse avec un seul caractère"""
    assert reverse_string("a") == "a"


def test_count_vowels_normal():
    """Test count_vowels avec voyelles"""
    assert count_vowels("hello") == 2


def test_count_vowels_all_vowels():
    """Test count_vowels avec toutes voyelles"""
    assert count_vowels("aeiou") == 5


def test_count_vowels_no_vowels():
    """Test count_vowels sans voyelles"""
    assert count_vowels("bcdfg") == 0


def test_count_vowels_uppercase():
    """Test count_vowels avec majuscules"""
    assert count_vowels("HELLO") == 2


def test_is_palindrome_true():
    """Test palindrome valide"""
    assert is_palindrome("radar") == True


def test_is_palindrome_false():
    """Test non-palindrome"""
    assert is_palindrome("hello") == False


def test_is_palindrome_with_spaces():
    """Test palindrome avec espaces"""
    assert is_palindrome("A man a plan a canal Panama") == True

"""
Lancez les tests:
"""
pytest tests/test_string_utils.py -v

"""
Vous devriez avoir 10 tests qui passent!


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

Vous avez appris:
[OK] Pourquoi tester (confiance, documentation, rapidité)
[OK] Structure de projet (src/, tests/)
[OK] Conventions de nommage (test_*.py, test_*())
[OK] Votre premier test avec assert
[OK] pytest.raises() pour les exceptions
[OK] Options CLI (-v, -x, -s, -k)
[OK] Organisation des tests

Points clés:
[CLE] test_*.py = pytest trouve automatiquement
[CLE] test_*() = fonction de test
[CLE] assert = vérification simple
[CLE] pytest.raises() = tester exceptions
[CLE] ARRANGE-ACT-ASSERT = structure d'un test


[OBJECTIF] AVANT DE CONTINUER:

Assurez-vous de comprendre:
1. Pourquoi on teste (pas juste "parce qu'il faut")
2. Comment écrire un test simple avec assert
3. Comment lancer pytest
4. Comment lire l'output de pytest

Si ce n'est pas clair, relisez ce chapitre et faites l'exercice!


PROCHAINS CHAPITRES:
- Chapitre 2: Assertions avancées
- Chapitre 3: Fixtures
- Chapitre 4: Parametrize
- Chapitre 5: Mocking
- Et bien plus!
"""

# ============================================================================
# [LIVRE] PYTEST - GUIDE ULTRA-DÉTAILLÉ PARTIE 2
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE:
# - Assertions avancées (comparaisons, collections, exceptions)
# - Messages d'erreur personnalisés
# - Assertions sur les types
# - Assertions approximatives (floats)
# - Warnings et logs
#
# [TEMPS] TEMPS DE LECTURE: ~2-3 heures
# [DOCS] PRÉREQUIS: Avoir complété la Partie 1
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 2: ASSERTIONS AVANCÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Maîtriser toutes les formes d'assertions pour tester efficacement.


[REFLEXION] COMPRENDRE ASSERT
-------------------

assert = mot-clé Python qui vérifie une condition

SYNTAXE DE BASE:
"""

assert condition, "Message optionnel si échec"

"""
COMMENT ÇA MARCHE?

Si condition est True:
    -> Continue normalement
    -> Rien ne se passe

Si condition est False:
    -> Lève AssertionError
    -> Le test échoue
    -> Pytest affiche le message


EXEMPLE SIMPLE:
"""

def test_basic_assertion():
    """
    [IDEE] ASSERTION LA PLUS SIMPLE
    """
    x = 5
    assert x == 5  # True -> test passe
    assert x > 0   # True -> test passe
    assert x < 10  # True -> test passe


def test_failing_assertion():
    """
    [IDEE] ASSERTION QUI ÉCHOUE
    """
    x = 5
    assert x == 10  # False -> test échoue!
    # Message d'erreur automatique de pytest:
    # assert 5 == 10


"""
ASSERTIONS AVEC COMPARAISONS
---------------------------

[IDEE] TOUS LES OPÉRATEURS DE COMPARAISON:
"""

def test_equality():
    """Test d'égalité"""
    assert 5 == 5           # Égalité
    assert "hello" == "hello"
    assert [1, 2] == [1, 2]


def test_inequality():
    """Test d'inégalité"""
    assert 5 != 6           # Différent
    assert "hello" != "world"


def test_comparisons():
    """Tests de comparaisons numériques"""
    assert 10 > 5           # Plus grand
    assert 5 < 10           # Plus petit
    assert 10 >= 10         # Plus grand ou égal
    assert 5 <= 5           # Plus petit ou égal


def test_identity():
    """Test d'identité (même objet en mémoire)"""
    a = [1, 2, 3]
    b = a  # b pointe vers le même objet que a
    c = [1, 2, 3]  # c est un nouvel objet
    
    assert a is b       # True (même objet)
    assert a is not c   # True (objets différents)
    assert a == c       # True (même valeur)


def test_membership():
    """Test d'appartenance"""
    fruits = ["apple", "banana", "orange"]
    
    assert "apple" in fruits        # True
    assert "grape" not in fruits    # True
    assert "a" in "banana"          # True (substring)
    assert 2 in [1, 2, 3]          # True


"""
[IDEE] ASTUCE PYTEST:

Pytest réécrit les assertions pour donner plus d'infos!

Code:
    assert a == b

Si échec, pytest affiche:
    assert 5 == 6
    
Pas juste "AssertionError"!


ASSERTIONS SUR LES CHAÎNES
-------------------------
"""

def test_string_assertions():
    """
    [IDEE] TESTER DES CHAÎNES DE CARACTÈRES
    
    Plusieurs façons de tester!
    """
    text = "Hello, World!"
    
    # Égalité exacte
    assert text == "Hello, World!"
    
    # Contient
    assert "World" in text
    assert "xyz" not in text
    
    # Commence par
    assert text.startswith("Hello")
    
    # Finit par
    assert text.endswith("World!")
    
    # Longueur
    assert len(text) == 13
    
    # Majuscules/minuscules
    assert text.upper() == "HELLO, WORLD!"
    assert text.lower() == "hello, world!"
    
    # Espaces
    spaced = "  hello  "
    assert spaced.strip() == "hello"


def test_multiline_strings():
    """
    [IDEE] TESTER DES STRINGS MULTILIGNES
    
    Pytest affiche les différences ligne par ligne!
    """
    actual = """Line 1
Line 2
Line 3"""
    
    expected = """Line 1
Line 2
Line 3"""
    
    assert actual == expected


"""
Si les strings diffèrent, pytest affiche:
"""
AssertionError: assert 'Line 1\nLin...ine 3' == 'Line 1\nLin...ine 3'
  - Line 3
  + Line Three

"""
Très utile pour comparer de longs textes!


ASSERTIONS SUR LES LISTES
------------------------
"""

def test_list_assertions():
    """
    [IDEE] TESTER DES LISTES
    
    Plusieurs niveaux de vérification!
    """
    numbers = [1, 2, 3, 4, 5]
    
    # Égalité exacte
    assert numbers == [1, 2, 3, 4, 5]
    
    # Longueur
    assert len(numbers) == 5
    
    # Vide ou non
    assert numbers  # True si non vide
    assert not []   # True si vide
    
    # Contient élément
    assert 3 in numbers
    assert 10 not in numbers
    
    # Premier/dernier élément
    assert numbers[0] == 1
    assert numbers[-1] == 5
    
    # Sous-liste
    assert numbers[1:3] == [2, 3]


def test_list_order_matters():
    """
    [IDEE] ATTENTION: L'ORDRE COMPTE!
    """
    # Ces assertions échouent car ordre différent
    # assert [1, 2, 3] == [3, 2, 1]  # [X]
    
    # Pour ignorer l'ordre, utiliser set
    assert set([1, 2, 3]) == set([3, 2, 1])  # [OK]


def test_list_contains_all():
    """
    [IDEE] VÉRIFIER QUE TOUS LES ÉLÉMENTS SONT PRÉSENTS
    """
    actual = [1, 2, 3, 4, 5]
    expected_elements = [2, 4]
    
    # Vérifier que tous les éléments attendus sont présents
    for element in expected_elements:
        assert element in actual


def test_list_type_of_elements():
    """
    [IDEE] VÉRIFIER LES TYPES D'ÉLÉMENTS
    """
    numbers = [1, 2, 3, 4, 5]
    
    # Tous les éléments sont des int
    assert all(isinstance(x, int) for x in numbers)
    
    # Au moins un élément > 3
    assert any(x > 3 for x in numbers)


"""
ASSERTIONS SUR LES DICTIONNAIRES
-------------------------------
"""

def test_dict_assertions():
    """
    [IDEE] TESTER DES DICTIONNAIRES
    
    Plusieurs approches!
    """
    user = {
        "name": "Alice",
        "age": 30,
        "email": "alice@example.com"
    }
    
    # Égalité exacte
    assert user == {
        "name": "Alice",
        "age": 30,
        "email": "alice@example.com"
    }
    
    # Clé existe
    assert "name" in user
    assert "phone" not in user
    
    # Valeur d'une clé
    assert user["name"] == "Alice"
    assert user.get("name") == "Alice"
    
    # Nombre de clés
    assert len(user) == 3
    
    # Liste des clés
    assert set(user.keys()) == {"name", "age", "email"}
    
    # Liste des valeurs
    assert "Alice" in user.values()


def test_dict_subset():
    """
    [IDEE] VÉRIFIER UN SOUS-ENSEMBLE DE CLÉS
    
    Utile quand le dict a beaucoup de clés!
    """
    response = {
        "id": 1,
        "name": "Alice",
        "age": 30,
        "created_at": "2024-01-01",
        "updated_at": "2024-01-02",
        # ... plein d'autres clés
    }
    
    # Vérifier seulement les clés importantes
    assert response["name"] == "Alice"
    assert response["age"] == 30
    
    # Ou créer un sous-dict
    important_fields = {k: response[k] for k in ["name", "age"]}
    assert important_fields == {"name": "Alice", "age": 30}


"""
ASSERTIONS SUR LES TUPLES ET SETS
--------------------------------
"""

def test_tuple_assertions():
    """
    [IDEE] TESTER DES TUPLES
    
    Comme les listes mais immuables
    """
    coords = (10, 20)
    
    # Égalité
    assert coords == (10, 20)
    
    # Longueur
    assert len(coords) == 2
    
    # Décomposition
    x, y = coords
    assert x == 10
    assert y == 20


def test_set_assertions():
    """
    [IDEE] TESTER DES SETS
    
    Ordre n'a pas d'importance!
    """
    fruits = {"apple", "banana", "orange"}
    
    # Égalité (ordre ne compte pas)
    assert fruits == {"orange", "apple", "banana"}
    
    # Contient
    assert "apple" in fruits
    
    # Taille
    assert len(fruits) == 3
    
    # Opérations ensemblistes
    veggies = {"carrot", "potato"}
    assert fruits.isdisjoint(veggies)  # Aucun élément en commun
    
    citrus = {"orange", "lemon"}
    assert fruits.intersection(citrus) == {"orange"}


"""
ASSERTIONS SUR LES BOOLÉENS
--------------------------
"""

def test_boolean_assertions():
    """
    [IDEE] TESTER DES BOOLÉENS
    
    True/False et valeurs truthy/falsy
    """
    # Booléens directs
    assert True
    assert not False
    
    # Valeurs truthy (considérées comme True)
    assert 1           # Nombres non-zéro
    assert "hello"     # Strings non-vides
    assert [1, 2, 3]   # Collections non-vides
    assert {"key": "value"}
    
    # Valeurs falsy (considérées comme False)
    assert not 0       # Zéro
    assert not ""      # String vide
    assert not []      # Liste vide
    assert not {}      # Dict vide
    assert not None    # None


def test_is_true_vs_equals_true():
    """
    [IDEE] DIFFÉRENCE SUBTILE
    
    is True vs == True
    """
    value = 1
    
    # == True: conversion implicite
    assert value == True  # 1 == True -> True
    
    # is True: identité stricte
    # assert value is True  # [X] 1 is not True
    
    # Préférez tester directement
    assert value  # [OK] Plus pythonique


"""
ASSERTIONS SUR LES NOMBRES
-------------------------
"""

def test_numeric_assertions():
    """
    [IDEE] TESTER DES NOMBRES
    """
    x = 42
    y = 3.14159
    
    # Égalité
    assert x == 42
    assert y == 3.14159
    
    # Comparaisons
    assert x > 40
    assert x < 50
    assert 40 < x < 50  # Chaînage!
    
    # Type
    assert isinstance(x, int)
    assert isinstance(y, float)
    
    # Signe
    assert x > 0  # Positif
    # assert x < 0  # Négatif
    
    # Pair/impair
    assert x % 2 == 0  # Pair
    assert 43 % 2 != 0  # Impair


def test_float_comparison():
    """
    [IDEE] PROBLÈME AVEC LES FLOATS
    
    Les floats ont des problèmes de précision!
    """
    # [X] MAUVAIS: Comparaison exacte
    result = 0.1 + 0.2
    # assert result == 0.3  # Échoue! 0.30000000000000004 != 0.3
    
    # [OK] BON: Utiliser pytest.approx()
    import pytest
    assert result == pytest.approx(0.3)
    
    # Avec tolérance personnalisée
    assert result == pytest.approx(0.3, abs=1e-6)
    
    # Ou tolérance relative
    assert 100.1 == pytest.approx(100, rel=0.01)  # 1% de tolérance


"""
[IDEE] PYTEST.APPROX() EN DÉTAIL:

pytest.approx(expected, rel=1e-6, abs=1e-12)

rel: Tolérance relative (par défaut 1e-6 = 0.0001%)
abs: Tolérance absolue (par défaut 1e-12)

QUAND UTILISER?
- Toujours pour comparer des floats!
- Calculs mathématiques
- Résultats d'APIs externes


ASSERTIONS SUR LES EXCEPTIONS
----------------------------
"""

def test_exception_basic():
    """
    [IDEE] TESTER QU'UNE EXCEPTION EST LEVÉE
    
    Vu au chapitre 1, mais approfondissons!
    """
    import pytest
    
    def divide(a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b
    
    # Test que l'exception est levée
    with pytest.raises(ValueError):
        divide(10, 0)


def test_exception_message():
    """
    [IDEE] VÉRIFIER LE MESSAGE D'ERREUR
    
    Vous pouvez aussi tester le message!
    """
    import pytest
    
    def validate_age(age):
        if age < 0:
            raise ValueError("Age must be positive")
        if age > 150:
            raise ValueError("Age too high")
    
    # Vérifier le message exact
    with pytest.raises(ValueError, match="Age must be positive"):
        validate_age(-5)
    
    # Match accepte des regex!
    with pytest.raises(ValueError, match=r"Age must be \w+"):
        validate_age(-5)


def test_exception_details():
    """
    [IDEE] ACCÉDER AUX DÉTAILS DE L'EXCEPTION
    
    Capturer l'exception pour tester plus de choses!
    """
    import pytest
    
    def create_user(age):
        if age < 18:
            raise ValueError("Too young", {"min_age": 18, "provided": age})
    
    # Capturer l'exception dans une variable
    with pytest.raises(ValueError) as exc_info:
        create_user(15)
    
    # Tester les détails
    assert "Too young" in str(exc_info.value)
    assert exc_info.value.args[1]["min_age"] == 18


def test_multiple_exceptions():
    """
    [IDEE] TESTER PLUSIEURS EXCEPTIONS POSSIBLES
    """
    import pytest
    
    def risky_operation(x):
        if x < 0:
            raise ValueError("Negative")
        if x == 0:
            raise ZeroDivisionError("Zero")
        return 1 / x
    
    # Test ValueError
    with pytest.raises(ValueError):
        risky_operation(-1)
    
    # Test ZeroDivisionError
    with pytest.raises(ZeroDivisionError):
        risky_operation(0)


"""
ASSERTIONS SUR LES TYPES
-----------------------
"""

def test_type_assertions():
    """
    [IDEE] VÉRIFIER LES TYPES D'OBJETS
    
    Plusieurs méthodes!
    """
    value = "hello"
    
    # isinstance (RECOMMANDÉ)
    assert isinstance(value, str)
    
    # type() (moins flexible)
    assert type(value) == str
    
    # Plusieurs types possibles
    number = 42
    assert isinstance(number, (int, float))
    
    # Type personnalisé
    class User:
        pass
    
    user = User()
    assert isinstance(user, User)


def test_none_assertions():
    """
    [IDEE] TESTER None
    
    Attention à is vs ==
    """
    value = None
    
    # RECOMMANDÉ: utiliser is
    assert value is None
    assert value is not "something"
    
    # Fonctionne mais moins pythonique
    assert value == None


"""
ASSERTIONS MULTIPLES
-------------------

[IDEE] PATTERN: Tester plusieurs conditions en une fois
"""

def test_multiple_assertions():
    """
    [IDEE] PLUSIEURS ASSERTIONS DANS UN TEST
    
    Si la première échoue, les autres ne sont pas exécutées!
    """
    user = {
        "name": "Alice",
        "age": 30,
        "email": "alice@example.com"
    }
    
    # Chaque assert peut échouer
    assert user["name"] == "Alice"
    assert user["age"] == 30
    assert user["email"] == "alice@example.com"
    
    # [ATTENTION] Si la première échoue, les autres ne sont pas testées!


def test_all_assertions_with_pytest():
    """
    [IDEE] VOIR TOUTES LES ERREURS D'UN COUP
    
    Utiliser pytest-check (plugin) pour voir toutes les assertions
    """
    # Nécessite: pip install pytest-check
    # import pytest_check as check
    
    # check.equal(1, 2)  # Note l'erreur mais continue
    # check.equal(3, 4)  # Note cette erreur aussi
    # check.equal(5, 5)  # Ok
    
    # À la fin, affiche toutes les erreurs!


"""
MESSAGES D'ERREUR PERSONNALISÉS
------------------------------

[IDEE] Rendre vos tests plus clairs avec des messages!
"""

def test_custom_error_messages():
    """
    [IDEE] AJOUTER DES MESSAGES D'ERREUR
    
    Aide à comprendre POURQUOI un test échoue
    """
    user_age = 15
    min_age = 18
    
    # Sans message
    # assert user_age >= min_age  # assert 15 >= 18
    
    # Avec message clair
    assert user_age >= min_age, \
        f"User age {user_age} is below minimum {min_age}"
    
    # Message avec contexte
    items = [1, 2, 3]
    expected_length = 5
    assert len(items) == expected_length, \
        f"Expected {expected_length} items but got {len(items)}: {items}"


def test_descriptive_messages():
    """
    [IDEE] MESSAGES DESCRIPTIFS
    
    Expliquez QUEL test échoue et POURQUOI c'est un problème
    """
    response = {"status": "error", "code": 500}
    
    assert response["status"] == "success", \
        f"API call failed with status '{response['status']}' " \
        f"and code {response['code']}"


"""
[COURS] EXERCICE PRATIQUE #1: ASSERTIONS COMPLEXES
--------------------------------------------

Testez cette classe User:
"""

class User:
    def __init__(self, username, email, age):
        if not username:
            raise ValueError("Username required")
        if "@" not in email:
            raise ValueError("Invalid email")
        if age < 0:
            raise ValueError("Age must be positive")
        
        self.username = username
        self.email = email
        self.age = age
        self.active = True
        self.posts = []
    
    def add_post(self, title, content):
        if not title:
            raise ValueError("Title required")
        post = {"title": title, "content": content}
        self.posts.append(post)
        return post
    
    def deactivate(self):
        self.active = False

"""
MISSION: Écrivez des tests complets!

Tests à créer:
1. Création normale d'un user
2. Username vide -> ValueError
3. Email sans @ -> ValueError
4. Age négatif -> ValueError
5. User est actif par défaut
6. Ajouter un post
7. Titre de post vide -> ValueError
8. Désactiver un user


SOLUTION:
"""

import pytest

def test_user_creation_success():
    """Test création normale"""
    user = User("alice", "alice@example.com", 25)
    
    assert user.username == "alice"
    assert user.email == "alice@example.com"
    assert user.age == 25
    assert user.active is True
    assert user.posts == []


def test_user_creation_no_username():
    """Test username vide"""
    with pytest.raises(ValueError, match="Username required"):
        User("", "alice@example.com", 25)


def test_user_creation_invalid_email():
    """Test email invalide"""
    with pytest.raises(ValueError, match="Invalid email"):
        User("alice", "not-an-email", 25)


def test_user_creation_negative_age():
    """Test age négatif"""
    with pytest.raises(ValueError, match="Age must be positive"):
        User("alice", "alice@example.com", -5)


def test_user_is_active_by_default():
    """Test user actif par défaut"""
    user = User("alice", "alice@example.com", 25)
    assert user.active is True


def test_add_post_success():
    """Test ajout de post"""
    user = User("alice", "alice@example.com", 25)
    
    post = user.add_post("Hello", "This is my first post")
    
    assert post["title"] == "Hello"
    assert post["content"] == "This is my first post"
    assert len(user.posts) == 1
    assert user.posts[0] == post


def test_add_post_no_title():
    """Test post sans titre"""
    user = User("alice", "alice@example.com", 25)
    
    with pytest.raises(ValueError, match="Title required"):
        user.add_post("", "Content")


def test_deactivate_user():
    """Test désactivation"""
    user = User("alice", "alice@example.com", 25)
    
    assert user.active is True
    user.deactivate()
    assert user.active is False


"""
[DOCS] RÉCAPITULATIF DU CHAPITRE 2
-----------------------------

Vous avez appris:
[OK] Assertions de base (==, !=, <, >, in)
[OK] Assertions sur strings (startswith, contains)
[OK] Assertions sur collections (list, dict, set, tuple)
[OK] pytest.approx() pour floats
[OK] pytest.raises() pour exceptions (avec match)
[OK] Assertions sur types (isinstance)
[OK] Messages d'erreur personnalisés

Points clés:
[CLE] assert condition, "message optionnel"
[CLE] pytest.approx() pour comparer floats
[CLE] pytest.raises() pour tester exceptions
[CLE] match= pour vérifier messages d'erreur
[CLE] Messages clairs aident au debugging


[OBJECTIF] PATTERNS IMPORTANTS:

1. COMPARAISONS FLOATS:
   assert result == pytest.approx(expected)

2. EXCEPTIONS:
   with pytest.raises(ExceptionType, match="message"):
       code_qui_doit_echouer()

3. COLLECTIONS:
   assert set(actual) == set(expected)  # Ignorer ordre

4. MESSAGES:
   assert condition, f"Clear message with {context}"


PROCHAIN CHAPITRE:
Fixtures - Réutiliser du code de setup entre tests!
"""

# ============================================================================
# [LIVRE] PYTEST - GUIDE ULTRA-DÉTAILLÉ PARTIE 3
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE:
# - Fixtures basiques et avancées
# - Scopes de fixtures (function, class, module, session)
# - Fixtures paramétrées
# - Fixtures avec yield (setup/teardown)
# - conftest.py
# - Fixtures builtin de pytest
#
# [TEMPS] TEMPS DE LECTURE: ~3-4 heures
# [DOCS] PRÉREQUIS: Avoir complété les Parties 1 et 2
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 3: FIXTURES - CONCEPT ET BASES
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Comprendre et maîtriser les fixtures pour éviter la duplication de code.


[REFLEXION] POURQUOI LES FIXTURES?
------------------------

PROBLÈME SANS FIXTURES:
"""

# tests/test_user_without_fixtures.py
def test_user_creation():
    """Test 1: Créer un user"""
    # Setup répétitif
    db = create_database()
    user = User("alice", "alice@example.com")
    db.save(user)
    
    # Test
    assert user.username == "alice"
    
    # Cleanup répétitif
    db.delete(user)
    db.close()


def test_user_update():
    """Test 2: Mettre à jour un user"""
    # Setup répétitif (ENCORE!)
    db = create_database()
    user = User("alice", "alice@example.com")
    db.save(user)
    
    # Test
    user.username = "alice_updated"
    db.save(user)
    assert user.username == "alice_updated"
    
    # Cleanup répétitif (ENCORE!)
    db.delete(user)
    db.close()


def test_user_deletion():
    """Test 3: Supprimer un user"""
    # Setup répétitif (ENCORE ET ENCORE!)
    db = create_database()
    user = User("alice", "alice@example.com")
    db.save(user)
    
    # Test
    db.delete(user)
    assert db.get(user.id) is None
    
    # Cleanup
    db.close()

"""
[X] PROBLÈMES:

1. DUPLICATION:
   - Même code de setup dans chaque test
   - Même code de cleanup dans chaque test
   - Si setup change -> modifier 10, 20, 100 tests!

2. OUBLIS:
   - Facile d'oublier le cleanup
   - Fuites de ressources (connexions DB, fichiers)

3. MAINTENANCE:
   - Difficile de changer le setup
   - Code verbeux et répétitif


[OK] SOLUTION: FIXTURES
"""

# tests/test_user_with_fixtures.py
import pytest

@pytest.fixture
def db():
    """
    [IDEE] FIXTURE: Code réutilisable de setup/teardown
    
    Créée UNE FOIS, utilisée PARTOUT!
    """
    # SETUP: Créer la ressource
    database = create_database()
    
    # DONNER la ressource au test
    yield database
    
    # TEARDOWN: Nettoyer (exécuté après le test)
    database.close()


@pytest.fixture
def sample_user(db):
    """
    [IDEE] FIXTURE AVEC DÉPENDANCE
    
    Peut utiliser d'autres fixtures!
    """
    user = User("alice", "alice@example.com")
    db.save(user)
    yield user
    db.delete(user)


# Tests deviennent SIMPLES!
def test_user_creation(sample_user):
    """Test 1: Juste le test, pas de setup!"""
    assert sample_user.username == "alice"


def test_user_update(db, sample_user):
    """Test 2: Setup automatique!"""
    sample_user.username = "alice_updated"
    db.save(sample_user)
    assert sample_user.username == "alice_updated"


def test_user_deletion(db, sample_user):
    """Test 3: Cleanup automatique!"""
    user_id = sample_user.id
    db.delete(sample_user)
    assert db.get(user_id) is None

"""
[IMPACT] AVANTAGES:

[OK] Pas de duplication
[OK] Setup/teardown centralisés
[OK] Tests lisibles (focus sur le test)
[OK] Facile à maintenir
[OK] Impossible d'oublier le cleanup


[IDEE] ANALOGIE:

SANS FIXTURES:
    Vous préparez votre café à chaque fois que vous en voulez
    -> Sortir la machine
    -> Mettre le café
    -> Faire chauffer l'eau
    -> Boire
    -> Nettoyer la machine

AVEC FIXTURES:
    La machine à café est déjà prête!
    -> Juste appuyer sur le bouton
    -> Boire
    -> (La machine se nettoie automatiquement)


CRÉER VOTRE PREMIÈRE FIXTURE
---------------------------
"""

import pytest

@pytest.fixture
def sample_list():
    """
    [IDEE] FIXTURE LA PLUS SIMPLE
    
    Retourne une valeur
    """
    return [1, 2, 3, 4, 5]


def test_list_length(sample_list):
    """
    [IDEE] UTILISER UNE FIXTURE
    
    Juste l'ajouter comme paramètre!
    """
    assert len(sample_list) == 5


def test_list_sum(sample_list):
    """
    [IDEE] MÊME FIXTURE, NOUVEAU TEST
    
    Chaque test reçoit une NOUVELLE instance!
    """
    assert sum(sample_list) == 15


"""
[REFLEXION] COMMENT ÇA MARCHE?

1. Pytest voit test_list_length(sample_list)
2. Cherche une fixture nommée "sample_list"
3. Exécute la fixture
4. Passe la valeur retournée au test
5. Le test s'exécute
6. Pytest nettoie (si teardown)


[IDEE] DÉTAIL IMPORTANT:

Chaque test reçoit une NOUVELLE instance!

def test_modify_list(sample_list):
    sample_list.append(6)
    assert len(sample_list) == 6

def test_original_list(sample_list):
    # C'est une NOUVELLE liste, pas modifiée!
    assert len(sample_list) == 5  # [OK] Passe!


FIXTURES AVEC SETUP SIMPLE
-------------------------
"""

@pytest.fixture
def temp_file():
    """
    [IDEE] FIXTURE QUI CRÉE UNE RESSOURCE
    
    return = valeur retournée au test
    """
    import tempfile
    
    # Créer un fichier temporaire
    file = tempfile.NamedTemporaryFile(mode='w', delete=False)
    file.write("Hello, World!")
    file.close()
    
    # Donner le chemin au test
    return file.name


def test_read_file(temp_file):
    """Test lecture de fichier"""
    with open(temp_file, 'r') as f:
        content = f.read()
    assert content == "Hello, World!"


"""
[ATTENTION] PROBLÈME: Le fichier n'est pas supprimé!

Chaque test crée un fichier qui reste sur le disque.


FIXTURES AVEC TEARDOWN (yield)
-----------------------------

[IDEE] PATTERN LE PLUS IMPORTANT!
"""

@pytest.fixture
def temp_file_with_cleanup():
    """
    [IDEE] FIXTURE AVEC SETUP ET TEARDOWN
    
    STRUCTURE:
    1. Code AVANT yield = Setup
    2. yield valeur = Donner au test
    3. Code APRÈS yield = Teardown
    """
    import tempfile
    import os
    
    # === SETUP ===
    file = tempfile.NamedTemporaryFile(mode='w', delete=False)
    file.write("Hello, World!")
    file.close()
    
    # === DONNER AU TEST ===
    yield file.name
    
    # === TEARDOWN (toujours exécuté!) ===
    os.remove(file.name)


def test_file_exists(temp_file_with_cleanup):
    """Pendant le test, le fichier existe"""
    import os
    assert os.path.exists(temp_file_with_cleanup)


# Après le test, le fichier est supprimé automatiquement!


"""
[IDEE] YIELD VS RETURN:

RETURN:
    @pytest.fixture
    def my_fixture():
        return value
    
    -> Pas de teardown
    -> Utilisez pour valeurs simples

YIELD:
    @pytest.fixture
    def my_fixture():
        setup()
        yield value
        teardown()
    
    -> Avec teardown
    -> Utilisez pour ressources (DB, fichiers, etc.)


FIXTURES AVEC GESTION D'ERREURS
------------------------------
"""

@pytest.fixture
def database_connection():
    """
    [IDEE] FIXTURE AVEC TRY/FINALLY
    
    Garantit que le teardown s'exécute même en cas d'erreur!
    """
    # Setup
    db = Database()
    db.connect()
    
    try:
        # Donner au test
        yield db
    finally:
        # Teardown TOUJOURS exécuté
        # Même si le test plante!
        db.disconnect()


"""
[IDEE] POURQUOI TRY/FINALLY?

Si le test lève une exception:
- Sans try/finally: teardown peut ne pas s'exécuter
- Avec try/finally: teardown TOUJOURS exécuté

GARANTIT le cleanup!


FIXTURES QUI DÉPENDENT D'AUTRES FIXTURES
---------------------------------------
"""

@pytest.fixture
def database():
    """Fixture 1: Base de données"""
    db = Database()
    db.connect()
    yield db
    db.disconnect()


@pytest.fixture
def user(database):
    """
    [IDEE] Fixture 2: Dépend de 'database'
    
    pytest résout automatiquement les dépendances!
    """
    user = User("alice", "alice@example.com")
    database.save(user)
    yield user
    database.delete(user)


@pytest.fixture
def post(database, user):
    """
    [IDEE] Fixture 3: Dépend de 'database' ET 'user'
    
    Plusieurs dépendances possibles!
    """
    post = Post(title="Hello", author=user)
    database.save(post)
    yield post
    database.delete(post)


def test_post_creation(post, user):
    """
    [IDEE] CHAÎNE DE DÉPENDANCES
    
    Ordre d'exécution:
    1. database (fixture)
    2. user (fixture, dépend de database)
    3. post (fixture, dépend de database et user)
    4. test_post_creation
    
    Teardown en ordre inverse:
    5. post teardown
    6. user teardown
    7. database teardown
    """
    assert post.author == user
    assert post.title == "Hello"


"""
[IDEE] PYTEST RÉSOUT L'ORDRE AUTOMATIQUEMENT!

Vous n'avez pas à vous soucier de l'ordre.
Pytest construit le graphe de dépendances.


[COURS] EXERCICE PRATIQUE #1: PREMIÈRE FIXTURE
----------------------------------------

Créez un système de fichiers temporaire:
"""

import pytest
import os
import tempfile
import shutil

@pytest.fixture
def temp_dir():
    """
    MISSION:
    1. Créer un dossier temporaire
    2. Le donner au test
    3. Le supprimer après le test
    
    Utilisez:
    - tempfile.mkdtemp() pour créer
    - shutil.rmtree() pour supprimer
    """
    # VOTRE CODE ICI
    pass


def test_create_file_in_temp_dir(temp_dir):
    """Test que le dossier fonctionne"""
    # Créer un fichier dedans
    file_path = os.path.join(temp_dir, "test.txt")
    with open(file_path, 'w') as f:
        f.write("Hello")
    
    # Vérifier qu'il existe
    assert os.path.exists(file_path)


"""
SOLUTION:
"""

@pytest.fixture
def temp_dir():
    """Fixture dossier temporaire"""
    # Setup: Créer dossier
    dir_path = tempfile.mkdtemp()
    
    # Donner au test
    yield dir_path
    
    # Teardown: Supprimer dossier
    shutil.rmtree(dir_path)


"""
FIXTURES AVEC CONFIGURATION
--------------------------
"""

@pytest.fixture
def api_client():
    """
    [IDEE] FIXTURE AVEC CONFIGURATION
    
    Initialiser un objet complexe
    """
    from myapp import APIClient
    
    client = APIClient(
        base_url="http://localhost:8000",
        timeout=30,
        retry=3
    )
    
    yield client
    
    # Pas de cleanup nécessaire ici
    # (ou client.close() si nécessaire)


def test_api_get(api_client):
    """Test API GET"""
    response = api_client.get("/users")
    assert response.status_code == 200


@pytest.fixture
def authenticated_client(api_client):
    """
    [IDEE] FIXTURE DÉRIVÉE
    
    Ajoute de l'authentification au client
    """
    # Login
    api_client.login("testuser", "password123")
    
    yield api_client
    
    # Logout
    api_client.logout()


def test_protected_endpoint(authenticated_client):
    """Test endpoint protégé"""
    response = authenticated_client.get("/profile")
    assert response.status_code == 200


"""
FIXTURES AVEC DONNÉES COMPLEXES
------------------------------
"""

@pytest.fixture
def sample_users():
    """
    [IDEE] FIXTURE AVEC DATASET COMPLEXE
    
    Utile pour avoir des données de test cohérentes
    """
    return [
        {
            "id": 1,
            "username": "alice",
            "email": "alice@example.com",
            "age": 25
        },
        {
            "id": 2,
            "username": "bob",
            "email": "bob@example.com",
            "age": 30
        },
        {
            "id": 3,
            "username": "charlie",
            "email": "charlie@example.com",
            "age": 35
        }
    ]


def test_filter_users_by_age(sample_users):
    """Test filtrage par âge"""
    young_users = [u for u in sample_users if u["age"] < 30]
    assert len(young_users) == 1
    assert young_users[0]["username"] == "alice"


@pytest.fixture
def sample_posts(sample_users):
    """
    [IDEE] FIXTURE QUI UTILISE SAMPLE_USERS
    
    Crée des posts pour les users
    """
    return [
        {
            "id": 1,
            "title": "First Post",
            "author_id": sample_users[0]["id"]
        },
        {
            "id": 2,
            "title": "Second Post",
            "author_id": sample_users[1]["id"]
        }
    ]


def test_posts_have_authors(sample_posts, sample_users):
    """Test relation posts/users"""
    for post in sample_posts:
        # Trouver l'auteur
        author = next(
            u for u in sample_users 
            if u["id"] == post["author_id"]
        )
        assert author is not None


"""
FIXTURES POUR MOCKING
--------------------

[IDEE] Créer des mocks réutilisables
"""

@pytest.fixture
def mock_email_service(monkeypatch):
    """
    [IDEE] FIXTURE POUR MOCKER
    
    monkeypatch = fixture builtin de pytest
    """
    sent_emails = []
    
    def fake_send_email(to, subject, body):
        """Fausse fonction d'envoi d'email"""
        sent_emails.append({
            "to": to,
            "subject": subject,
            "body": body
        })
        return True
    
    # Remplacer la vraie fonction
    monkeypatch.setattr(
        "myapp.email.send_email",
        fake_send_email
    )
    
    # Retourner la liste pour vérification
    return sent_emails


def test_user_registration_sends_email(mock_email_service):
    """Test que l'inscription envoie un email"""
    from myapp import register_user
    
    register_user("alice", "alice@example.com")
    
    # Vérifier qu'un email a été envoyé
    assert len(mock_email_service) == 1
    assert mock_email_service[0]["to"] == "alice@example.com"
    assert "Welcome" in mock_email_service[0]["subject"]


"""
[DOCS] RÉCAPITULATIF DU CHAPITRE 3
-----------------------------

Vous avez appris:
[OK] Pourquoi utiliser des fixtures (DRY)
[OK] @pytest.fixture pour créer une fixture
[OK] Paramètre de fonction pour utiliser une fixture
[OK] yield pour setup/teardown
[OK] Fixtures qui dépendent d'autres fixtures
[OK] try/finally pour garantir cleanup
[OK] Fixtures pour données de test
[OK] Fixtures pour mocking

Points clés:
[CLE] @pytest.fixture = code réutilisable
[CLE] yield = donner valeur + permettre teardown
[CLE] Nom du paramètre = nom de la fixture
[CLE] Fixtures peuvent dépendre d'autres fixtures
[CLE] try/finally garantit cleanup


[OBJECTIF] PATTERN FONDAMENTAL:

@pytest.fixture
def my_resource():
    # Setup
    resource = create_resource()
    
    # Donner au test
    yield resource
    
    # Teardown (toujours exécuté)
    cleanup_resource(resource)


PROCHAIN CHAPITRE:
Scopes de fixtures - Contrôler la durée de vie!
"""


# ============================================================================
# [GUIDE] CHAPITRE 4: SCOPES DE FIXTURES
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Optimiser vos tests avec les scopes de fixtures.


[REFLEXION] LE PROBLÈME DE PERFORMANCE
----------------------------

Imaginez une fixture qui prend du temps:
"""

@pytest.fixture
def database():
    """
    [X] PROBLÈME: Exécutée à chaque test!
    
    Si vous avez 100 tests:
    -> 100 connexions/déconnexions de DB
    -> Lent! (peut-être 10 secondes)
    """
    db = Database()
    db.connect()  # Lent (1 seconde)
    yield db
    db.disconnect()


def test_1(database):
    # Connexion DB (1 sec)
    assert database.query("SELECT 1")
    # Déconnexion


def test_2(database):
    # Connexion DB ENCORE (1 sec)
    assert database.query("SELECT 2")
    # Déconnexion


# 100 tests = 100 secondes juste pour les connexions!

"""
[IDEE] SOLUTION: SCOPES

Un scope = durée de vie d'une fixture

SCOPES DISPONIBLES:
1. function (défaut) : Nouvelle instance par test
2. class : Partagée par tests d'une classe
3. module : Partagée par tests d'un fichier
4. package : Partagée par tests d'un package
5. session : Partagée par TOUS les tests


SCOPE: FUNCTION (défaut)
-----------------------
"""

@pytest.fixture(scope="function")
def fresh_list():
    """
    [IDEE] SCOPE FUNCTION (défaut)
    
    Nouvelle instance pour CHAQUE test
    
    QUAND UTILISER?
    - Données modifiables
    - Tests qui modifient la fixture
    - Isolation maximale
    """
    return [1, 2, 3]


def test_append_to_list(fresh_list):
    fresh_list.append(4)
    assert len(fresh_list) == 4


def test_list_is_fresh(fresh_list):
    # Nouvelle liste! Pas de 4
    assert len(fresh_list) == 3  # [OK]


"""
[IDEE] DIAGRAMME D'EXÉCUTION:

Test 1:
    fresh_list() -> [1, 2, 3]
    test_append_to_list
    teardown

Test 2:
    fresh_list() -> [1, 2, 3]  <- NOUVELLE instance!
    test_list_is_fresh
    teardown


SCOPE: CLASS
-----------
"""

@pytest.fixture(scope="class")
def database_class_scope():
    """
    [IDEE] SCOPE CLASS
    
    Partagée par tous les tests d'UNE classe
    
    QUAND UTILISER?
    - Ressource coûteuse à créer
    - Tests regroupés logiquement
    - Tests ne modifient pas la fixture
    """
    db = Database()
    db.connect()
    yield db
    db.disconnect()


class TestUserOperations:
    """
    [IDEE] GROUPE DE TESTS
    
    Tous partagent la même fixture!
    """
    
    def test_create_user(self, database_class_scope):
        # Utilise la DB
        user = User("alice")
        database_class_scope.save(user)
        assert user.id is not None
    
    def test_read_user(self, database_class_scope):
        # MÊME DB qu'avant!
        user = database_class_scope.get(User, 1)
        assert user is not None
    
    def test_update_user(self, database_class_scope):
        # Toujours la MÊME DB
        user = database_class_scope.get(User, 1)
        user.name = "alice_updated"
        database_class_scope.save(user)


"""
[IDEE] DIAGRAMME D'EXÉCUTION:

TestUserOperations:
    database_class_scope() -> connexion
    
    test_create_user       <- Partage
    test_read_user         <- Partage
    test_update_user       <- Partage
    
    teardown -> déconnexion


[ATTENTION] ATTENTION:

Les tests PARTAGENT la fixture!
- test_create_user crée un user
- test_read_user peut le lire
- Ordre des tests peut importer!

DANGER: Tests interdépendants


SCOPE: MODULE
------------
"""

@pytest.fixture(scope="module")
def database_module_scope():
    """
    [IDEE] SCOPE MODULE
    
    Partagée par TOUS les tests du fichier
    
    QUAND UTILISER?
    - Ressource très coûteuse (API, DB)
    - Tests en lecture seule
    - Optimisation performance
    """
    print("\n>>> Setup database (module scope)")
    db = Database()
    db.connect()
    yield db
    db.disconnect()
    print("\n>>> Teardown database (module scope)")


# test_users.py

def test_user_1(database_module_scope):
    print("Test 1")
    assert database_module_scope.is_connected()


def test_user_2(database_module_scope):
    print("Test 2")
    assert database_module_scope.is_connected()


class TestUserClass:
    def test_user_3(self, database_module_scope):
        print("Test 3")
        assert database_module_scope.is_connected()


"""
[IDEE] OUTPUT:

>>> Setup database (module scope)
Test 1
Test 2
Test 3
>>> Teardown database (module scope)

Setup une SEULE fois pour tout le fichier!


SCOPE: SESSION
-------------
"""

@pytest.fixture(scope="session")
def database_session_scope():
    """
    [IDEE] SCOPE SESSION
    
    Partagée par TOUS les tests de TOUTE la suite!
    
    QUAND UTILISER?
    - Setup extrêmement coûteux
    - Ressources globales (serveur de test)
    - Maximum d'optimisation
    
    [ATTENTION] DANGER:
    - Tests peuvent s'influencer mutuellement
    - Difficile à debugger si problème
    """
    print("\n>>> Setup database (SESSION SCOPE)")
    db = Database()
    db.connect()
    db.create_tables()  # Coûteux!
    yield db
    db.drop_tables()
    db.disconnect()
    print("\n>>> Teardown database (SESSION SCOPE)")


"""
[IDEE] DIAGRAMME D'EXÉCUTION:

Suite de tests complète:
    database_session_scope() -> Setup UNE FOIS
    
    test_file_1.py:
        test_1
        test_2
    
    test_file_2.py:
        test_3
        test_4
    
    Teardown UNE FOIS


COMPARAISON DES SCOPES
---------------------

Exemple avec timing:
"""

import time

@pytest.fixture(scope="function")
def slow_fixture_function():
    time.sleep(0.1)  # 100ms
    return "data"


@pytest.fixture(scope="module")
def slow_fixture_module():
    time.sleep(0.1)  # 100ms
    return "data"


# Avec scope="function" et 10 tests:
# 10 tests × 100ms = 1 seconde

# Avec scope="module" et 10 tests:
# 1 × 100ms = 100ms total

"""
[IDEE] GAIN DE PERFORMANCE POTENTIEL ÉNORME!


AUTOUSE: FIXTURES AUTOMATIQUES
-----------------------------
"""

@pytest.fixture(autouse=True)
def reset_database():
    """
    [IDEE] AUTOUSE = Exécutée AUTOMATIQUEMENT
    
    Pas besoin de l'ajouter comme paramètre!
    
    QUAND UTILISER?
    - Setup nécessaire pour TOUS les tests
    - Nettoyage systématique
    - Logging/monitoring
    """
    # Avant chaque test
    Database.reset()
    
    yield
    
    # Après chaque test (optionnel)
    pass


def test_user_creation():
    """
    [IDEE] PAS besoin de paramètre reset_database!
    
    La fixture s'exécute quand même automatiquement.
    """
    user = User("alice")
    assert user.username == "alice"


"""
[IDEE] AUTOUSE AVEC SCOPE:
"""

@pytest.fixture(scope="module", autouse=True)
def setup_test_environment():
    """
    [IDEE] AUTOUSE + MODULE SCOPE
    
    Exécuté une fois au début du module
    Sans besoin de l'importer!
    """
    print("\n>>> Setting up test environment")
    # Configuration globale
    os.environ["TESTING"] = "true"
    
    yield
    
    print("\n>>> Tearing down test environment")
    os.environ.pop("TESTING", None)


"""
[ATTENTION] UTILISER AUTOUSE AVEC PARCIMONIE!

Peut rendre les tests difficiles à comprendre:
- Comportement "magique" invisible
- Difficile de savoir quelles fixtures s'exécutent


FIXTURES AVEC PARAMÈTRES (prélude)
---------------------------------

On peut passer des arguments indirectement:
"""

@pytest.fixture(scope="module")
def api_client_with_config():
    """
    [IDEE] FIXTURE AVEC CONFIGURATION
    
    Utilise des variables d'environnement ou config
    """
    config = {
        "base_url": os.getenv("API_URL", "http://localhost:8000"),
        "timeout": int(os.getenv("API_TIMEOUT", "30"))
    }
    
    client = APIClient(**config)
    yield client
    client.close()


"""
[COURS] EXERCICE PRATIQUE #2: OPTIMISER AVEC SCOPES
---------------------------------------------

Situation:
- 50 tests utilisent une connexion DB
- Connexion prend 2 secondes
- Tests sont en lecture seule

MISSION:
1. Créer une fixture de DB avec le bon scope
2. Mesurer le temps gagné


SOLUTION:
"""

import pytest
import time

# [X] LENT: scope="function"
@pytest.fixture(scope="function")
def db_slow():
    """2 secondes × 50 tests = 100 secondes!"""
    time.sleep(2)
    db = Database()
    yield db
    db.close()


# [OK] RAPIDE: scope="module"
@pytest.fixture(scope="module")
def db_fast():
    """2 secondes × 1 fois = 2 secondes!"""
    time.sleep(2)
    db = Database()
    yield db
    db.close()


# 50 tests en lecture seule
def test_read_1(db_fast):
    assert db_fast.query("SELECT 1")

# ... 49 autres tests similaires


"""
Gain: 98 secondes (98% plus rapide!)


SCOPES ET TEARDOWN
-----------------

Le teardown dépend du scope:
"""

@pytest.fixture(scope="module")
def shared_resource():
    """
    [IDEE] TEARDOWN APRÈS TOUS LES TESTS DU MODULE
    """
    print("\n>>> Setup shared resource")
    resource = ExpensiveResource()
    
    yield resource
    
    # Exécuté une fois, après TOUS les tests du module
    print("\n>>> Cleanup shared resource")
    resource.cleanup()


def test_1(shared_resource):
    print("Test 1 using resource")


def test_2(shared_resource):
    print("Test 2 using resource")


def test_3(shared_resource):
    print("Test 3 using resource")


"""
OUTPUT:

>>> Setup shared resource
Test 1 using resource
Test 2 using resource
Test 3 using resource
>>> Cleanup shared resource

Cleanup une SEULE fois à la fin!


FIXTURES AVEC SCOPES DIFFÉRENTS
------------------------------

Que se passe-t-il si les scopes diffèrent?
"""

@pytest.fixture(scope="session")
def app():
    """Application - session scope"""
    return create_app()


@pytest.fixture(scope="function")
def client(app):
    """
    [IDEE] CLIENT DÉPEND D'APP
    
    app: scope="session"
    client: scope="function"
    
    RÈGLE: Scope plus large peut être utilisé par scope plus petit
    """
    return app.test_client()


def test_1(client):
    # app créée une fois
    # client créé pour ce test
    response = client.get("/")
    assert response.status_code == 200


def test_2(client):
    # MÊME app
    # NOUVEAU client
    response = client.get("/users")
    assert response.status_code == 200


"""
[IDEE] DIAGRAMME:

Session:
    app() <- Une seule fois
    
    Test 1:
        client(app) <- Nouveau
        test_1
    
    Test 2:
        client(app) <- Nouveau
        test_2


[ATTENTION] IMPOSSIBLE: Scope plus petit ne peut pas être utilisé par plus large!
"""

@pytest.fixture(scope="function")
def temp_data():
    return {"value": 1}


@pytest.fixture(scope="module")
def processor(temp_data):  # [X] ERREUR!
    """
    ScopeMismatch: Fixture 'processor' (module scope)
    cannot use fixture 'temp_data' (function scope)
    """
    return DataProcessor(temp_data)


"""
POURQUOI?

processor est créé une fois pour le module
Mais temp_data est recréé à chaque test
-> Incohérent!


CHOISIR LE BON SCOPE
-------------------

[IDEE] GUIDE DE DÉCISION:

function (défaut):
[OK] Tests modifient la fixture
[OK] Isolation maximale nécessaire
[OK] Fixture légère (< 100ms)
[X] Fixture coûteuse utilisée souvent

class:
[OK] Tests logiquement groupés
[OK] Fixture moyennement coûteuse
[X] Tests modifient la fixture
[X] Tests doivent être indépendants

module:
[OK] Fixture coûteuse (> 500ms)
[OK] Tests en lecture seule
[OK] Même fichier de tests
[X] Plusieurs fichiers l'utilisent

session:
[OK] Fixture TRÈS coûteuse (> 5s)
[OK] Ressource globale (serveur)
[OK] Utilisée partout
[X] Tests peuvent se marcher dessus


EXEMPLE RÉALISTE: API TESTING
----------------------------
"""

import pytest
import time

@pytest.fixture(scope="session")
def api_server():
    """
    [IDEE] SERVEUR API - SESSION SCOPE
    
    Démarrer un serveur prend 5 secondes
    Une seule fois pour tous les tests!
    """
    print("\n>>> Starting API server...")
    time.sleep(5)
    server = APIServer(port=5000)
    server.start()
    
    yield server
    
    print("\n>>> Stopping API server...")
    server.stop()


@pytest.fixture(scope="module")
def database(api_server):
    """
    [IDEE] DATABASE - MODULE SCOPE
    
    Partagée par tests du module
    Dépend du serveur
    """
    print("\n>>> Connecting to database...")
    db = Database()
    db.connect()
    
    yield db
    
    print("\n>>> Disconnecting from database...")
    db.disconnect()


@pytest.fixture(scope="function")
def authenticated_client(api_server):
    """
    [IDEE] CLIENT AUTHENTIFIÉ - FUNCTION SCOPE
    
    Nouveau pour chaque test (token peut expirer)
    """
    client = api_server.client()
    client.login("testuser", "password")
    
    yield client
    
    client.logout()


# Tests
def test_create_user(authenticated_client, database):
    response = authenticated_client.post("/users", json={"name": "Alice"})
    assert response.status_code == 201


def test_get_users(authenticated_client, database):
    response = authenticated_client.get("/users")
    assert response.status_code == 200


"""
[IDEE] RÉSULTAT:

1 démarrage serveur (5s)
1 connexion DB par module
1 client authentifié par test

Au lieu de:
N démarrages serveur
N connexions DB
N authentifications

Gain énorme!


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

Vous avez appris:
[OK] Les 5 scopes (function, class, module, package, session)
[OK] @pytest.fixture(scope="...")
[OK] autouse=True pour fixtures automatiques
[OK] Règles de compatibilité des scopes
[OK] Optimisation de performance
[OK] Guide de décision pour choisir le scope

Points clés:
[CLE] scope="function" = défaut, isolation maximale
[CLE] scope="module" = partagée dans un fichier
[CLE] scope="session" = partagée partout
[CLE] Scope large peut être utilisé par scope étroit
[CLE] autouse=True = exécution automatique


[OBJECTIF] RÈGLE D'OR:

Commencez avec scope="function" (sûr)
Optimisez seulement si nécessaire:
- Tests lents
- Fixture coûteuse
- Pas de modification de la fixture


PROCHAIN CHAPITRE:
Fixtures paramétrées - Plusieurs variantes d'une fixture!
"""

```

Continuons avec la Partie 4 (Fixtures paramétrées et conftest.py) ?

# PARTIE 4: FIXTURES PARAMÉTRÉES ET CONFTEST.PY

```python
# ============================================================================
# [LIVRE] PYTEST - GUIDE ULTRA-DÉTAILLÉ PARTIE 4
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE:
# - Fixtures paramétrées (params)
# - conftest.py (partage de fixtures)
# - Fixtures builtin de pytest
# - Request object
# - Finalizers
# - Factory fixtures
#
# [TEMPS] TEMPS DE LECTURE: ~3-4 heures
# [DOCS] PRÉREQUIS: Avoir complété les Parties 1, 2 et 3
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 5: FIXTURES PARAMÉTRÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Créer une fixture qui fournit plusieurs valeurs différentes.


[REFLEXION] LE PROBLÈME: TESTER AVEC PLUSIEURS VALEURS
--------------------------------------------

Vous voulez tester une fonction avec différents inputs:
"""

def normalize_email(email):
    """Normalise un email en lowercase"""
    return email.lower().strip()


# Test avec plusieurs emails
def test_normalize_gmail():
    assert normalize_email("User@Gmail.com") == "user@gmail.com"


def test_normalize_yahoo():
    assert normalize_email("User@Yahoo.com") == "user@yahoo.com"


def test_normalize_outlook():
    assert normalize_email("User@Outlook.com") == "user@outlook.com"


"""
[X] PROBLÈMES:

1. DUPLICATION:
   - Même structure de test répétée
   - Code difficile à maintenir

2. INCOMPLET:
   - Facile d'oublier un cas
   - Pas systématique


SOLUTION 1: PARAMETRIZE (vu plus tard)
"""

@pytest.mark.parametrize("email,expected", [
    ("User@Gmail.com", "user@gmail.com"),
    ("User@Yahoo.com", "user@yahoo.com"),
    ("User@Outlook.com", "user@outlook.com"),
])
def test_normalize_email(email, expected):
    assert normalize_email(email) == expected


"""
SOLUTION 2: FIXTURE PARAMÉTRÉE
-----------------------------

[IDEE] Quand utiliser une fixture paramétrée vs parametrize?

FIXTURE PARAMÉTRÉE:
[OK] Ressource à initialiser différemment (DB, API client)
[OK] Setup complexe qui varie
[OK] Plusieurs tests utilisent les mêmes variantes

PARAMETRIZE:
[OK] Tester une fonction avec plusieurs inputs
[OK] Cas de test simples
[OK] Pas de setup complexe


CRÉER UNE FIXTURE PARAMÉTRÉE
---------------------------
"""

import pytest

@pytest.fixture(params=[1, 2, 3])
def number(request):
    """
    [IDEE] FIXTURE PARAMÉTRÉE
    
    params = liste de valeurs
    request.param = valeur actuelle
    
    Le test s'exécutera 3 fois:
    - Une fois avec number=1
    - Une fois avec number=2
    - Une fois avec number=3
    """
    return request.param


def test_number_is_positive(number):
    """
    [IDEE] CE TEST S'EXÉCUTE 3 FOIS!
    
    Test 1: number=1
    Test 2: number=2
    Test 3: number=3
    """
    assert number > 0


"""
[IDEE] EXÉCUTION:

pytest -v

Output:
test_example.py::test_number_is_positive[1] PASSED
test_example.py::test_number_is_positive[2] PASSED
test_example.py::test_number_is_positive[3] PASSED


[REFLEXION] DÉCORTIQUONS:

@pytest.fixture(params=[1, 2, 3])
                ^^^^^^^^^^^^^^ Liste de valeurs

def number(request):
           ^^^^^^^ Objet spécial pytest

    return request.param
           ^^^^^^^^^^^^^ Valeur actuelle du paramètre


[IDEE] REQUEST OBJECT:

request = objet fourni automatiquement par pytest
request.param = valeur actuelle de params


EXEMPLE RÉALISTE: TESTER PLUSIEURS DATABASES
-------------------------------------------
"""

@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database(request):
    """
    [IDEE] FIXTURE POUR TESTER AVEC PLUSIEURS DATABASES!
    
    Chaque test s'exécutera 3 fois:
    - Avec SQLite
    - Avec PostgreSQL
    - Avec MySQL
    """
    db_type = request.param
    
    # Setup selon le type
    if db_type == "sqlite":
        db = SQLiteDatabase(":memory:")
    elif db_type == "postgres":
        db = PostgreSQLDatabase("localhost", "testdb")
    elif db_type == "mysql":
        db = MySQLDatabase("localhost", "testdb")
    
    db.connect()
    db.create_tables()
    
    yield db
    
    # Teardown
    db.drop_tables()
    db.disconnect()


def test_create_user(database):
    """
    [IDEE] S'EXÉCUTE 3 FOIS automatiquement!
    
    Vérifie que votre code fonctionne avec toutes les DB!
    """
    user = User(username="alice")
    database.save(user)
    
    retrieved = database.get(User, user.id)
    assert retrieved.username == "alice"


def test_update_user(database):
    """Aussi exécuté 3 fois!"""
    user = User(username="bob")
    database.save(user)
    
    user.username = "bob_updated"
    database.save(user)
    
    retrieved = database.get(User, user.id)
    assert retrieved.username == "bob_updated"


"""
[IDEE] RÉSULTAT:

6 tests exécutés au total!
- test_create_user[sqlite]
- test_create_user[postgres]
- test_create_user[mysql]
- test_update_user[sqlite]
- test_update_user[postgres]
- test_update_user[mysql]


PARAMS AVEC OBJETS COMPLEXES
---------------------------
"""

@pytest.fixture(params=[
    {"browser": "chrome", "headless": True},
    {"browser": "firefox", "headless": True},
    {"browser": "safari", "headless": False},
])
def browser(request):
    """
    [IDEE] PARAMS PEUT ÊTRE N'IMPORTE QUOI!
    
    Dicts, tuples, objets personnalisés, etc.
    """
    config = request.param
    
    if config["browser"] == "chrome":
        driver = ChromeDriver(headless=config["headless"])
    elif config["browser"] == "firefox":
        driver = FirefoxDriver(headless=config["headless"])
    elif config["browser"] == "safari":
        driver = SafariDriver(headless=config["headless"])
    
    yield driver
    
    driver.quit()


def test_page_load(browser):
    """S'exécute avec Chrome, Firefox et Safari!"""
    browser.get("https://example.com")
    assert browser.title == "Example Domain"


"""
IDS PERSONNALISÉS POUR PARAMS
----------------------------

Par défaut, pytest affiche [0], [1], [2]...
Vous pouvez personnaliser:
"""

@pytest.fixture(params=[
    ("chrome", True),
    ("firefox", True),
    ("safari", False)
], ids=["chrome-headless", "firefox-headless", "safari-headed"])
def browser_with_ids(request):
    """
    [IDEE] IDS = Noms personnalisés dans l'output
    
    Plus lisible que [0], [1], [2]!
    """
    browser_name, headless = request.param
    driver = create_driver(browser_name, headless)
    yield driver
    driver.quit()


"""
Output:
test_example.py::test_page_load[chrome-headless] PASSED
test_example.py::test_page_load[firefox-headless] PASSED
test_example.py::test_page_load[safari-headed] PASSED


IDS AVEC FONCTION
----------------
"""

def browser_id(param):
    """
    [IDEE] FONCTION POUR GÉNÉRER IDS DYNAMIQUEMENT
    """
    browser, headless = param
    return f"{browser}-{'headless' if headless else 'headed'}"


@pytest.fixture(params=[
    ("chrome", True),
    ("firefox", True),
    ("safari", False)
], ids=browser_id)
def browser_dynamic_ids(request):
    browser_name, headless = request.param
    driver = create_driver(browser_name, headless)
    yield driver
    driver.quit()


"""
PARAMS INDIRECT
--------------

Parfois vous voulez passer le param à la fixture mais pas l'utiliser directement:
"""

@pytest.fixture
def user(request):
    """
    [IDEE] FIXTURE QUI CRÉE UN USER
    
    Peut recevoir des params de parametrize
    """
    username = request.param
    user = User(username=username)
    user.save()
    yield user
    user.delete()


@pytest.mark.parametrize("user", ["alice", "bob", "charlie"], indirect=True)
def test_user_creation(user):
    """
    [IDEE] INDIRECT = TRUE
    
    Les valeurs sont passées à la fixture
    au lieu d'être utilisées directement
    """
    assert user.username in ["alice", "bob", "charlie"]
    assert user.id is not None


"""
[IDEE] DIFFÉRENCE:

Sans indirect:
    user = "alice" (string directement)

Avec indirect:
    user = fixture qui reçoit "alice" et crée User object


COMBINER PLUSIEURS FIXTURES PARAMÉTRÉES
--------------------------------------
"""

@pytest.fixture(params=["sqlite", "postgres"])
def database_type(request):
    return request.param


@pytest.fixture(params=[True, False])
def use_cache(request):
    return request.param


def test_query_performance(database_type, use_cache):
    """
    [IDEE] PRODUIT CARTÉSIEN!
    
    2 databases × 2 cache options = 4 combinaisons:
    - sqlite + cache
    - sqlite + no cache
    - postgres + cache
    - postgres + no cache
    """
    db = create_db(database_type)
    if use_cache:
        db.enable_cache()
    
    result = db.query("SELECT 1")
    assert result is not None


"""
Output:
test[sqlite-True] PASSED
test[sqlite-False] PASSED
test[postgres-True] PASSED
test[postgres-False] PASSED


[ATTENTION] ATTENTION: EXPLOSION COMBINATOIRE!

2 fixtures × 3 valeurs chacune = 6 tests
3 fixtures × 3 valeurs chacune = 27 tests
4 fixtures × 3 valeurs chacune = 81 tests

Peut devenir TRÈS lent!


REQUEST OBJECT EN DÉTAIL
------------------------

request contient plus que juste param:
"""

@pytest.fixture
def example_fixture(request):
    """
    [IDEE] REQUEST OBJECT: Beaucoup d'infos!
    """
    print(f"\n=== Request Object ===")
    print(f"Param: {request.param if hasattr(request, 'param') else 'None'}")
    print(f"Function: {request.function.__name__}")
    print(f"Module: {request.module.__name__}")
    print(f"Fixture name: {request.fixturename}")
    print(f"Scope: {request.scope}")
    print(f"Node: {request.node}")
    
    # Config pytest
    print(f"Config: {request.config}")
    
    yield "data"


"""
UTILISER REQUEST POUR NOMS DYNAMIQUES
------------------------------------
"""

@pytest.fixture(params=["small", "medium", "large"])
def dataset(request):
    """
    [IDEE] CRÉER DATASET SELON LE PARAMÈTRE
    """
    size = request.param
    
    if size == "small":
        data = list(range(10))
    elif size == "medium":
        data = list(range(100))
    elif size == "large":
        data = list(range(1000))
    
    # Utiliser request pour logging
    print(f"\nCreating {size} dataset for test: {request.function.__name__}")
    
    return data


def test_process_data(dataset):
    """Test avec différentes tailles de dataset"""
    result = process(dataset)
    assert len(result) == len(dataset)


"""
[COURS] EXERCICE PRATIQUE #1: API CLIENT PARAMÉTRÉ
--------------------------------------------

MISSION:
Créer une fixture paramétrée pour tester une API avec:
- Différents formats de réponse (json, xml)
- Différents timeouts (5s, 10s, 30s)
"""

import pytest

@pytest.fixture(params=[
    # VOTRE CODE ICI
    # Créer combinaisons de format et timeout
])
def api_client(request):
    """
    Fixture qui crée un client API
    avec différentes configurations
    """
    # VOTRE CODE ICI
    pass


def test_api_get_users(api_client):
    """Test GET /users avec différentes configs"""
    # VOTRE CODE ICI
    pass


"""
SOLUTION:
"""

@pytest.fixture(params=[
    {"format": "json", "timeout": 5},
    {"format": "json", "timeout": 10},
    {"format": "xml", "timeout": 5},
    {"format": "xml", "timeout": 10},
], ids=lambda p: f"{p['format']}-{p['timeout']}s")
def api_client(request):
    """
    Fixture paramétrée pour API client
    
    4 combinaisons testées:
    - json, 5s timeout
    - json, 10s timeout
    - xml, 5s timeout
    - xml, 10s timeout
    """
    config = request.param
    
    client = APIClient(
        base_url="http://localhost:8000",
        response_format=config["format"],
        timeout=config["timeout"]
    )
    
    yield client
    
    client.close()


def test_api_get_users(api_client):
    """
    S'exécute 4 fois avec différentes configs!
    """
    response = api_client.get("/users")
    
    # Vérifier selon le format
    if api_client.response_format == "json":
        assert isinstance(response, dict)
    elif api_client.response_format == "xml":
        assert response.startswith("<?xml")


"""
[DOCS] RÉCAPITULATIF FIXTURES PARAMÉTRÉES
-----------------------------------

Vous avez appris:
[OK] @pytest.fixture(params=[...])
[OK] request.param pour accéder à la valeur
[OK] ids= pour noms personnalisés
[OK] Combinaison de fixtures paramétrées
[OK] indirect=True avec parametrize

Points clés:
[CLE] params = liste de valeurs
[CLE] Test s'exécute une fois par valeur
[CLE] request.param = valeur actuelle
[CLE] Combinaison de fixtures = produit cartésien
[CLE] Attention à l'explosion combinatoire


PROCHAIN CHAPITRE:
conftest.py - Partager des fixtures entre fichiers!
"""


# ============================================================================
# [GUIDE] CHAPITRE 6: CONFTEST.PY - PARTAGE DE FIXTURES
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Organiser et partager des fixtures dans toute votre suite de tests.


[REFLEXION] LE PROBLÈME: DUPLICATION DE FIXTURES
--------------------------------------

Structure actuelle:
"""

tests/
├── test_users.py
├── test_posts.py
└── test_comments.py

"""
Chaque fichier a besoin de la même fixture database:
"""

# test_users.py
@pytest.fixture
def database():
    db = Database()
    db.connect()
    yield db
    db.disconnect()

def test_create_user(database):
    # ...


# test_posts.py
@pytest.fixture
def database():  # [X] DUPLIQUÉ!
    db = Database()
    db.connect()
    yield db
    db.disconnect()

def test_create_post(database):
    # ...


# test_comments.py
@pytest.fixture
def database():  # [X] ENCORE DUPLIQUÉ!
    db = Database()
    db.connect()
    yield db
    db.disconnect()

def test_create_comment(database):
    # ...


"""
[X] PROBLÈMES:

1. DUPLICATION:
   - Même fixture copiée 3 fois
   - Si vous modifiez database, modifier 3 endroits!

2. INCOHÉRENCE:
   - Facile que les copies divergent
   - Bugs difficiles à trouver


[OK] SOLUTION: CONFTEST.PY
-----------------------

[IDEE] conftest.py = Fichier magique de pytest!

RÈGLES:
1. Nom obligatoire: conftest.py (exactement!)
2. Fixtures définies dedans sont automatiquement disponibles
3. Pas besoin d'importer!
4. Un conftest.py par dossier (hiérarchie possible)
"""

tests/
├── conftest.py          # <- Fixtures partagées!
├── test_users.py
├── test_posts.py
└── test_comments.py


# tests/conftest.py
"""
[IDEE] CONFTEST.PY: Fixtures centralisées
"""
import pytest

@pytest.fixture
def database():
    """
    Fixture partagée automatiquement!
    
    Disponible dans TOUS les fichiers de tests
    du dossier (et sous-dossiers)
    """
    db = Database()
    db.connect()
    yield db
    db.disconnect()


# tests/test_users.py
def test_create_user(database):
    """
    [IDEE] PAS BESOIN D'IMPORTER!
    
    pytest trouve automatiquement la fixture
    dans conftest.py
    """
    user = User("alice")
    database.save(user)
    assert user.id is not None


# tests/test_posts.py
def test_create_post(database):
    """Même fixture, zéro import!"""
    post = Post("Hello")
    database.save(post)
    assert post.id is not None


"""
[IMPACT] AVANTAGES:

[OK] Une seule définition
[OK] Disponible partout automatiquement
[OK] Pas d'imports nécessaires
[OK] Facile à maintenir


HIÉRARCHIE DE CONFTEST.PY
------------------------

Vous pouvez avoir plusieurs conftest.py:
"""

tests/
├── conftest.py              # Racine: fixtures globales
├── test_basic.py
├── unit/
│   ├── conftest.py          # Fixtures pour tests unitaires
│   ├── test_models.py
│   └── test_utils.py
└── integration/
    ├── conftest.py          # Fixtures pour tests d'intégration
    ├── test_api.py
    └── test_database.py


# tests/conftest.py (RACINE)
"""Fixtures globales pour tous les tests"""
@pytest.fixture(scope="session")
def app_config():
    """Config partagée partout"""
    return {
        "debug": False,
        "testing": True
    }


# tests/unit/conftest.py
"""Fixtures spécifiques aux tests unitaires"""
@pytest.fixture
def mock_database():
    """Mock de DB pour tests unitaires"""
    return MockDatabase()


# tests/integration/conftest.py
"""Fixtures spécifiques aux tests d'intégration"""
@pytest.fixture(scope="module")
def real_database():
    """Vraie DB pour tests d'intégration"""
    db = RealDatabase()
    db.connect()
    yield db
    db.disconnect()


"""
[IDEE] RÈGLES DE RÉSOLUTION:

Pytest cherche les fixtures dans cet ordre:
1. Même fichier de test
2. conftest.py du même dossier
3. conftest.py du dossier parent
4. conftest.py du parent du parent
5. ... jusqu'à la racine


EXEMPLE: tests/integration/api/test_users.py utilise 'database'

Recherche:
1. test_users.py -> Non trouvé
2. tests/integration/api/conftest.py -> Non trouvé
3. tests/integration/conftest.py -> Trouvé! (real_database)


FIXTURES LOCALES OVERRIDENT CONFTEST
-----------------------------------
"""

# tests/conftest.py
@pytest.fixture
def user():
    """User par défaut"""
    return User("default_user")


# tests/test_special.py
@pytest.fixture
def user():
    """
    [IDEE] OVERRIDE LA FIXTURE DE CONFTEST
    
    Juste pour ce fichier!
    """
    return User("special_user")


def test_with_special_user(user):
    """Utilise le user local, pas celui de conftest"""
    assert user.username == "special_user"


"""
[IDEE] PATTERN UTILE:

Fixture générique dans conftest
Override spécifique dans certains fichiers


ORGANISER CONFTEST.PY
--------------------

Pour un gros projet:
"""

# tests/conftest.py
"""
Organisation recommandée:
1. Imports
2. Configuration
3. Fixtures de base
4. Fixtures dérivées
5. Hooks pytest (avancé)
"""

# === IMPORTS ===
import pytest
import os
from myapp import create_app
from myapp.database import Database

# === CONFIGURATION ===
@pytest.fixture(scope="session")
def config():
    """Configuration globale"""
    return {
        "database_url": os.getenv("TEST_DB_URL", "sqlite:///:memory:"),
        "secret_key": "test-secret-key",
        "debug": False
    }


# === FIXTURES DE BASE ===
@pytest.fixture(scope="session")
def app(config):
    """Application Flask/FastAPI"""
    app = create_app(config)
    return app


@pytest.fixture(scope="module")
def database(config):
    """Connexion base de données"""
    db = Database(config["database_url"])
    db.connect()
    db.create_tables()
    
    yield db
    
    db.drop_tables()
    db.disconnect()


# === FIXTURES DÉRIVÉES ===
@pytest.fixture
def client(app):
    """Client de test"""
    return app.test_client()


@pytest.fixture
def authenticated_client(client):
    """Client authentifié"""
    client.login("testuser", "password")
    return client


@pytest.fixture
def sample_user(database):
    """User de test"""
    user = User("alice", "alice@example.com")
    database.save(user)
    yield user
    database.delete(user)


@pytest.fixture
def sample_users(database):
    """Plusieurs users de test"""
    users = [
        User("alice", "alice@example.com"),
        User("bob", "bob@example.com"),
        User("charlie", "charlie@example.com")
    ]
    for user in users:
        database.save(user)
    
    yield users
    
    for user in users:
        database.delete(user)


"""
[IDEE] CONVENTIONS:

- Grouper par thème
- Commenter les sections
- Fixtures simples en haut
- Fixtures complexes en bas
- Ordre de dépendances visible


CONFTEST.PY PAR DOMAINE
----------------------

Pour très gros projets:
"""

tests/
├── conftest.py                    # Global
├── unit/
│   ├── conftest.py               # Fixtures unitaires
│   ├── models/
│   │   ├── conftest.py           # Fixtures models
│   │   └── test_user.py
│   └── services/
│       ├── conftest.py           # Fixtures services
│       └── test_email.py
└── integration/
    ├── conftest.py               # Fixtures intégration
    ├── api/
    │   ├── conftest.py           # Fixtures API
    │   └── test_endpoints.py
    └── database/
        ├── conftest.py           # Fixtures DB
        └── test_queries.py


"""
Chaque niveau a ses fixtures spécialisées!


FIXTURES DANS CONFTEST AVEC AUTOUSE
----------------------------------
"""

# tests/conftest.py
@pytest.fixture(autouse=True)
def reset_database():
    """
    [IDEE] AUTOUSE dans conftest = Appliqué à TOUS les tests!
    
    Utile pour:
    - Nettoyage systématique
    - Logging
    - Setup d'environnement
    """
    # Avant chaque test
    Database.reset()
    
    yield
    
    # Après chaque test
    Database.rollback()


@pytest.fixture(scope="session", autouse=True)
def setup_test_environment():
    """
    [IDEE] Setup GLOBAL au début de la session
    """
    print("\n>>> Setting up test environment")
    os.environ["TESTING"] = "1"
    
    yield
    
    print("\n>>> Tearing down test environment")
    os.environ.pop("TESTING", None)


"""
[ATTENTION] ATTENTION avec autouse dans conftest!

Peut avoir effets de bord inattendus
Utilisez seulement pour setup vraiment global


PLUGINS ET CONFTEST
------------------

conftest.py peut aussi contenir:
"""

# tests/conftest.py
def pytest_configure(config):
    """
    [IDEE] HOOK PYTEST: Configuration
    
    Appelé au début de pytest
    """
    config.addinivalue_line(
        "markers", "slow: marks tests as slow"
    )
    config.addinivalue_line(
        "markers", "integration: marks tests as integration tests"
    )


def pytest_collection_modifyitems(config, items):
    """
    [IDEE] HOOK PYTEST: Modifier les tests collectés
    
    Exemple: Ajouter un marker à tous les tests d'un dossier
    """
    for item in items:
        if "integration" in str(item.fspath):
            item.add_marker(pytest.mark.integration)


"""
(Hooks pytest = sujet avancé, voir documentation)


[COURS] EXERCICE PRATIQUE #2: ORGANISER AVEC CONFTEST
-----------------------------------------------

MISSION:
Créer une structure de conftest.py pour un projet FastAPI

Structure:
"""

tests/
├── conftest.py
├── unit/
│   └── test_models.py
└── integration/
    └── test_api.py

"""
Fixtures nécessaires:
1. app (FastAPI app)
2. database (connexion DB)
3. client (TestClient)
4. authenticated_client (avec token)


SOLUTION:
"""

# tests/conftest.py
"""Configuration globale des tests"""
import pytest
import os
from fastapi.testclient import TestClient
from myapp.main import create_app
from myapp.database import Database, Base
from myapp.core.security import create_access_token

# === CONFIGURATION ===
@pytest.fixture(scope="session")
def test_config():
    """Configuration pour tests"""
    return {
        "database_url": "sqlite:///:memory:",
        "secret_key": "test-secret-key-change-in-production",
        "testing": True
    }


# === APPLICATION ===
@pytest.fixture(scope="session")
def app(test_config):
    """Application FastAPI"""
    app = create_app(test_config)
    return app


# === DATABASE ===
@pytest.fixture(scope="function")
def database(test_config):
    """Base de données de test"""
    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker
    
    engine = create_engine(test_config["database_url"])
    TestingSessionLocal = sessionmaker(bind=engine)
    
    # Créer tables
    Base.metadata.create_all(bind=engine)
    
    db = TestingSessionLocal()
    
    yield db
    
    # Cleanup
    db.close()
    Base.metadata.drop_all(bind=engine)


# === CLIENT HTTP ===
@pytest.fixture
def client(app, database):
    """Client de test"""
    # Override la dépendance get_db
    def override_get_db():
        try:
            yield database
        finally:
            pass
    
    app.dependency_overrides[get_db] = override_get_db
    
    with TestClient(app) as test_client:
        yield test_client
    
    app.dependency_overrides.clear()


# === CLIENT AUTHENTIFIÉ ===
@pytest.fixture
def authenticated_client(client, database):
    """Client avec token JWT"""
    # Créer un user de test
    from myapp.models import User
    from myapp.core.security import get_password_hash
    
    user = User(
        username="testuser",
        email="test@example.com",
        hashed_password=get_password_hash("testpass123")
    )
    database.add(user)
    database.commit()
    
    # Créer token
    token = create_access_token(data={"sub": user.username})
    
    # Ajouter header au client
    client.headers = {
        **client.headers,
        "Authorization": f"Bearer {token}"
    }
    
    return client


# tests/unit/test_models.py
"""Tests unitaires des modèles"""
from myapp.models import User

def test_user_creation(database):
    """Test création d'un user"""
    user = User(username="alice", email="alice@example.com")
    database.add(user)
    database.commit()
    
    assert user.id is not None
    assert user.username == "alice"


# tests/integration/test_api.py
"""Tests d'intégration de l'API"""

def test_get_users_unauthenticated(client):
    """Test GET /users sans auth"""
    response = client.get("/users")
    assert response.status_code == 401


def test_get_users_authenticated(authenticated_client):
    """Test GET /users avec auth"""
    response = authenticated_client.get("/users")
    assert response.status_code == 200


"""
[DOCS] RÉCAPITULATIF DU CHAPITRE 6
-----------------------------

Vous avez appris:
[OK] conftest.py pour partager fixtures
[OK] Hiérarchie de conftest.py
[OK] Fixtures automatiquement disponibles
[OK] Override de fixtures locales
[OK] Organisation de conftest.py
[OK] autouse dans conftest
[OK] Hooks pytest (introduction)

Points clés:
[CLE] conftest.py = fixtures partagées automatiquement
[CLE] Pas besoin d'importer!
[CLE] Hiérarchie: fichier -> dossier -> parent -> ...
[CLE] Fixture locale override conftest
[CLE] Un conftest.py par niveau d'organisation


[OBJECTIF] STRUCTURE RECOMMANDÉE:

tests/
├── conftest.py              <- Fixtures globales
├── unit/
│   ├── conftest.py         <- Fixtures unitaires
│   └── test_*.py
└── integration/
    ├── conftest.py         <- Fixtures intégration
    └── test_*.py


PROCHAIN CHAPITRE:
Fixtures builtin de pytest - Outils fournis!
"""

```

Continuons avec la Partie 5 (Fixtures builtin et patterns avancés) ?

# ============================================================================
# [LIVRE] PYTEST - GUIDE ULTRA-DÉTAILLÉ PARTIE 5
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE:
# - Fixtures builtin de pytest (tmp_path, monkeypatch, capsys, etc.)
# - Factory fixtures
# - Finalizers
# - Fixtures avancées
# - Patterns et best practices
#
# [TEMPS] TEMPS DE LECTURE: ~3-4 heures
# [DOCS] PRÉREQUIS: Avoir complété les Parties 1-4
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 7: FIXTURES BUILTIN DE PYTEST
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Maîtriser les fixtures fournies par pytest.


[REFLEXION] FIXTURES BUILTIN: QU'EST-CE QUE C'EST?
----------------------------------------

Pytest fournit des fixtures prêtes à l'emploi!

[IDEE] Pas besoin de les créer, juste les utiliser!

LISTE DES PRINCIPALES FIXTURES BUILTIN:
- tmp_path, tmp_path_factory : Fichiers temporaires
- monkeypatch : Modifier/mocker du code
- capsys, capfd : Capturer stdout/stderr
- request : Infos sur le test en cours
- pytestconfig : Configuration pytest
- cache : Cache entre exécutions
- record_property, record_xml_attribute : Rapports
- recwarn : Capturer warnings


TMP_PATH: FICHIERS TEMPORAIRES
-----------------------------
"""

import pytest

def test_create_file(tmp_path):
    """
    [IDEE] TMP_PATH: Dossier temporaire unique par test!
    
    tmp_path = objet pathlib.Path
    Créé automatiquement avant le test
    Supprimé automatiquement après
    
    QUAND UTILISER?
    - Tests qui créent/lisent des fichiers
    - Tests qui nécessitent un filesystem
    - Isolation garantie (chaque test = dossier unique)
    """
    # tmp_path pointe vers un dossier temporaire unique
    # Ex: /tmp/pytest-of-user/pytest-current/test_create_file0/
    
    # Créer un fichier dedans
    test_file = tmp_path / "test.txt"
    test_file.write_text("Hello, World!")
    
    # Vérifier
    assert test_file.exists()
    assert test_file.read_text() == "Hello, World!"
    
    # Après le test, tout est supprimé automatiquement!


def test_create_subdirectory(tmp_path):
    """Créer une structure de dossiers"""
    # Créer sous-dossier
    subdir = tmp_path / "data"
    subdir.mkdir()
    
    # Créer fichier dedans
    data_file = subdir / "data.json"
    data_file.write_text('{"key": "value"}')
    
    # Vérifier structure
    assert subdir.exists()
    assert subdir.is_dir()
    assert data_file.exists()
    assert data_file.is_file()


def test_multiple_files(tmp_path):
    """Créer plusieurs fichiers"""
    files = []
    for i in range(5):
        file = tmp_path / f"file_{i}.txt"
        file.write_text(f"Content {i}")
        files.append(file)
    
    # Vérifier qu'ils existent tous
    assert len(list(tmp_path.iterdir())) == 5
    for file in files:
        assert file.exists()


"""
[IDEE] TMP_PATH VS TMP_PATH_FACTORY

tmp_path (scope="function"):
    - Nouveau dossier par test
    - Scope function uniquement

tmp_path_factory (scope="session"):
    - Créer dossiers temporaires dans fixtures de scope plus large
"""

@pytest.fixture(scope="module")
def shared_temp_dir(tmp_path_factory):
    """
    [IDEE] TMP_PATH_FACTORY
    
    Pour créer dossiers temporaires dans fixtures
    avec scope > function
    """
    # Créer dossier temporaire
    temp_dir = tmp_path_factory.mktemp("shared_data")
    
    # Setup: créer des fichiers partagés
    for i in range(10):
        file = temp_dir / f"data_{i}.txt"
        file.write_text(f"Shared content {i}")
    
    yield temp_dir
    
    # Cleanup automatique par pytest


def test_use_shared_dir_1(shared_temp_dir):
    """Test 1 utilisant le dossier partagé"""
    files = list(shared_temp_dir.glob("*.txt"))
    assert len(files) == 10


def test_use_shared_dir_2(shared_temp_dir):
    """Test 2 utilisant le MÊME dossier"""
    first_file = shared_temp_dir / "data_0.txt"
    assert first_file.read_text() == "Shared content 0"


"""
MONKEYPATCH: MODIFIER/MOCKER DU CODE
-----------------------------------

[IDEE] MONKEYPATCH = Remplacer temporairement du code pendant un test
"""

def test_monkeypatch_function(monkeypatch):
    """
    [IDEE] MONKEYPATCH: Remplacer une fonction
    
    Utile pour:
    - Mocker des appels API
    - Mocker des fonctions système
    - Modifier comportement sans changer le code
    """
    # Fonction originale
    def original_function():
        return "original"
    
    # Fonction de remplacement
    def mock_function():
        return "mocked"
    
    # Remplacer
    monkeypatch.setattr("module.original_function", mock_function)
    
    # Maintenant, appeler module.original_function() retourne "mocked"


def test_monkeypatch_environment(monkeypatch):
    """
    [IDEE] MODIFIER VARIABLES D'ENVIRONNEMENT
    
    Très utile pour tester du code qui dépend de l'environnement!
    """
    # Avant
    import os
    assert os.getenv("MY_VAR") is None
    
    # Modifier
    monkeypatch.setenv("MY_VAR", "test_value")
    
    # Pendant le test
    assert os.getenv("MY_VAR") == "test_value"
    
    # Après le test, automatiquement restauré!


def test_monkeypatch_delete_env(monkeypatch):
    """Supprimer une variable d'environnement"""
    import os
    
    # Créer une variable
    os.environ["TEMP_VAR"] = "value"
    
    # Supprimer pour le test
    monkeypatch.delenv("TEMP_VAR", raising=False)
    
    # N'existe plus
    assert os.getenv("TEMP_VAR") is None


def test_monkeypatch_sys_path(monkeypatch):
    """
    [IDEE] MODIFIER sys.path
    
    Utile pour tests d'import
    """
    import sys
    
    # Ajouter un chemin temporairement
    monkeypatch.syspath_prepend("/custom/path")
    
    assert "/custom/path" in sys.path


"""
EXEMPLE RÉALISTE: MOCKER UN APPEL API
"""

# Code à tester
import requests

def get_weather(city):
    """Récupère la météo depuis une API externe"""
    response = requests.get(f"http://api.weather.com/v1/{city}")
    return response.json()


# Test avec monkeypatch
def test_get_weather(monkeypatch):
    """
    [IDEE] MOCKER requests.get pour ne pas appeler la vraie API!
    """
    # Créer une fausse réponse
    class MockResponse:
        def json(self):
            return {"temp": 20, "condition": "sunny"}
    
    def mock_get(*args, **kwargs):
        return MockResponse()
    
    # Remplacer requests.get
    monkeypatch.setattr("requests.get", mock_get)
    
    # Appeler la fonction (utilise le mock!)
    result = get_weather("Paris")
    
    assert result["temp"] == 20
    assert result["condition"] == "sunny"


"""
MONKEYPATCH: MÉTHODES PRINCIPALES

monkeypatch.setattr(target, value)
    -> Remplacer un attribut/fonction

monkeypatch.delattr(target)
    -> Supprimer un attribut

monkeypatch.setitem(dict, key, value)
    -> Modifier un dictionnaire

monkeypatch.delitem(dict, key)
    -> Supprimer une clé

monkeypatch.setenv(name, value)
    -> Modifier variable d'environnement

monkeypatch.delenv(name)
    -> Supprimer variable d'environnement

monkeypatch.syspath_prepend(path)
    -> Ajouter au début de sys.path

monkeypatch.chdir(path)
    -> Changer le working directory


CAPSYS: CAPTURER STDOUT/STDERR
-----------------------------
"""

def test_print_output(capsys):
    """
    [IDEE] CAPSYS: Capturer print() et autres sorties
    
    QUAND UTILISER?
    - Tester du code qui print()
    - Vérifier les logs
    - Tester les messages d'erreur
    """
    # Code qui print
    print("Hello")
    print("World")
    
    # Capturer la sortie
    captured = capsys.readouterr()
    
    # Vérifier
    assert captured.out == "Hello\nWorld\n"
    assert captured.err == ""  # Pas d'erreurs


def test_stderr_output(capsys):
    """Capturer stderr"""
    import sys
    
    print("Normal output")
    print("Error output", file=sys.stderr)
    
    captured = capsys.readouterr()
    
    assert captured.out == "Normal output\n"
    assert captured.err == "Error output\n"


def test_multiple_captures(capsys):
    """
    [IDEE] CAPTURER PLUSIEURS FOIS
    
    readouterr() vide le buffer après lecture
    """
    print("First")
    captured1 = capsys.readouterr()
    assert captured1.out == "First\n"
    
    print("Second")
    captured2 = capsys.readouterr()
    assert captured2.out == "Second\n"


def test_disable_capture(capsys):
    """
    [IDEE] DÉSACTIVER TEMPORAIREMENT LA CAPTURE
    """
    print("Captured")
    
    with capsys.disabled():
        print("Not captured (visible dans le terminal)")
    
    print("Captured again")
    
    captured = capsys.readouterr()
    assert "Captured" in captured.out
    assert "Not captured" not in captured.out


"""
EXEMPLE RÉALISTE: TESTER UN CLI
"""

def greet_user(name):
    """Fonction CLI simple"""
    print(f"Hello, {name}!")
    print("Welcome to our app")


def test_greet_user(capsys):
    """Tester la sortie du CLI"""
    greet_user("Alice")
    
    captured = capsys.readouterr()
    
    assert "Hello, Alice!" in captured.out
    assert "Welcome to our app" in captured.out


"""
CAPFD VS CAPSYS

capsys: Capture stdout/stderr au niveau Python
capfd: Capture au niveau file descriptor (plus bas niveau)

Utilisez capsys dans la plupart des cas!


PYTESTCONFIG: CONFIGURATION
--------------------------
"""

def test_config_access(pytestconfig):
    """
    [IDEE] PYTESTCONFIG: Accéder à la config pytest
    
    Utile pour tests conditionnels basés sur config
    """
    # Accéder aux options CLI
    verbose = pytestconfig.option.verbose
    
    # Accéder à rootdir
    rootdir = pytestconfig.rootdir
    
    # Accéder aux ini values
    markers = pytestconfig.getini("markers")


"""
REQUEST: INFORMATIONS SUR LE TEST
--------------------------------

(Déjà vu avec les fixtures paramétrées)
"""

def test_request_info(request):
    """
    [IDEE] REQUEST: Métadonnées du test
    """
    print(f"\nTest function: {request.function.__name__}")
    print(f"Test module: {request.module.__name__}")
    print(f"Test fspath: {request.fspath}")
    print(f"Test node: {request.node}")


"""
CACHE: PERSISTANCE ENTRE EXÉCUTIONS
----------------------------------
"""

def test_cache_values(cache):
    """
    [IDEE] CACHE: Stocker des valeurs entre exécutions de pytest!
    
    Le cache persiste même après que pytest se termine!
    
    QUAND UTILISER?
    - Optimiser tests lents
    - Mémoriser résultats
    - Statistiques
    """
    # Récupérer valeur du cache (None si pas existe)
    last_run = cache.get("last_run", None)
    
    print(f"\nLast run: {last_run}")
    
    # Stocker dans le cache
    from datetime import datetime
    cache.set("last_run", str(datetime.now()))
    
    # La prochaine fois que pytest run, la valeur sera là!


"""
Le cache est stocké dans .pytest_cache/


RECWARN: CAPTURER WARNINGS
-------------------------
"""

def test_warnings(recwarn):
    """
    [IDEE] RECWARN: Capturer les warnings Python
    
    QUAND UTILISER?
    - Vérifier qu'un warning est émis
    - Vérifier qu'aucun warning n'est émis
    """
    import warnings
    
    # Émettre un warning
    warnings.warn("This is a warning", UserWarning)
    
    # Vérifier
    assert len(recwarn) == 1
    warning = recwarn.pop(UserWarning)
    assert str(warning.message) == "This is a warning"


def test_no_warnings(recwarn):
    """Vérifier qu'aucun warning n'est émis"""
    # Code qui ne doit pas émettre de warnings
    result = 1 + 1
    
    # Vérifier
    assert len(recwarn) == 0


"""
[COURS] EXERCICE PRATIQUE #1: UTILISER LES FIXTURES BUILTIN
-----------------------------------------------------

MISSION:
Tester une fonction qui:
1. Lit un fichier de config
2. Appelle une API externe
3. Print le résultat
4. Émet un warning si le résultat est vide
"""

import requests
import warnings

def process_config(config_file):
    """Fonction à tester"""
    # 1. Lire config
    with open(config_file) as f:
        config = f.read()
    
    # 2. Appeler API
    response = requests.get("http://api.example.com/data")
    data = response.json()
    
    # 3. Print
    print(f"Received {len(data)} items")
    
    # 4. Warning si vide
    if not data:
        warnings.warn("No data received", UserWarning)
    
    return data


# Testez cette fonction avec TOUTES les fixtures nécessaires!

"""
SOLUTION:
"""

def test_process_config(tmp_path, monkeypatch, capsys, recwarn):
    """
    [IDEE] TEST COMPLET avec PLUSIEURS fixtures builtin!
    """
    # 1. Créer fichier de config temporaire
    config_file = tmp_path / "config.txt"
    config_file.write_text("test config")
    
    # 2. Mocker requests.get
    class MockResponse:
        def json(self):
            return [{"id": 1}, {"id": 2}]
    
    def mock_get(*args, **kwargs):
        return MockResponse()
    
    monkeypatch.setattr("requests.get", mock_get)
    
    # 3. Appeler la fonction
    result = process_config(str(config_file))
    
    # 4. Vérifier le résultat
    assert len(result) == 2
    
    # 5. Vérifier le print
    captured = capsys.readouterr()
    assert "Received 2 items" in captured.out
    
    # 6. Vérifier pas de warning (car data non vide)
    assert len(recwarn) == 0


def test_process_config_empty_data(tmp_path, monkeypatch, recwarn):
    """Test avec données vides -> warning attendu"""
    # Config file
    config_file = tmp_path / "config.txt"
    config_file.write_text("test config")
    
    # Mock retournant données vides
    class MockResponse:
        def json(self):
            return []
    
    monkeypatch.setattr("requests.get", lambda *a, **k: MockResponse())
    
    # Appeler
    result = process_config(str(config_file))
    
    # Vérifier warning émis
    assert len(recwarn) == 1
    assert "No data received" in str(recwarn[0].message)


"""
[DOCS] RÉCAPITULATIF FIXTURES BUILTIN
--------------------------------

Vous avez appris:
[OK] tmp_path / tmp_path_factory : Fichiers temporaires
[OK] monkeypatch : Modifier du code temporairement
[OK] capsys / capfd : Capturer stdout/stderr
[OK] pytestconfig : Accéder à la configuration
[OK] request : Infos sur le test
[OK] cache : Persister des données
[OK] recwarn : Capturer warnings

Points clés:
[CLE] Fixtures builtin = prêtes à l'emploi
[CLE] Pas besoin de les créer
[CLE] Très utiles pour cas courants
[CLE] Combinables entre elles


[OBJECTIF] FIXTURES BUILTIN LES PLUS UTILES:

1. tmp_path : 90% des tests fichiers
2. monkeypatch : Mocker sans lib externe
3. capsys : Tester CLI et print()


PROCHAIN CHAPITRE:
Factory fixtures et patterns avancés!
"""


# ============================================================================
# [GUIDE] CHAPITRE 8: FACTORY FIXTURES ET PATTERNS AVANCÉS
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Maîtriser les patterns avancés de fixtures.


[REFLEXION] FACTORY FIXTURES: QU'EST-CE QUE C'EST?
----------------------------------------

PROBLÈME:

Vous voulez créer PLUSIEURS instances d'un objet dans un test:
"""

@pytest.fixture
def user():
    """Fixture classique: retourne UN user"""
    return User("alice")


def test_two_users(user):
    """
    [X] PROBLÈME: Comment avoir 2 users différents?
    
    user = UN seul user
    On ne peut pas faire user1 et user2!
    """
    # On voudrait:
    user1 = create_user("alice")
    user2 = create_user("bob")
    # ...


"""
[OK] SOLUTION: FACTORY FIXTURE

Une fixture qui retourne une FONCTION pour créer des objets!
"""

@pytest.fixture
def user_factory():
    """
    [IDEE] FACTORY FIXTURE: Retourne une fonction!
    
    Permet de créer autant d'instances qu'on veut
    """
    created_users = []
    
    def create_user(username, email=None):
        """Fonction factory"""
        if email is None:
            email = f"{username}@example.com"
        
        user = User(username, email)
        user.save()
        created_users.append(user)
        
        return user
    
    # Retourner la fonction!
    yield create_user
    
    # Cleanup: supprimer tous les users créés
    for user in created_users:
        user.delete()


def test_multiple_users(user_factory):
    """
    [IDEE] UTILISATION: Appeler la factory autant de fois que nécessaire!
    """
    # Créer plusieurs users
    alice = user_factory("alice")
    bob = user_factory("bob")
    charlie = user_factory("charlie")
    
    # Tests
    assert alice.username == "alice"
    assert bob.username == "bob"
    assert charlie.username == "charlie"
    
    # Tous les users seront supprimés automatiquement!


"""
[IDEE] AVANTAGES FACTORY FIXTURE:

[OK] Créer autant d'instances que nécessaire
[OK] Avec des paramètres différents
[OK] Cleanup automatique de toutes les instances
[OK] Code DRY


FACTORY FIXTURE AVEC PARAMÈTRES PAR DÉFAUT
-----------------------------------------
"""

@pytest.fixture
def post_factory(user_factory):
    """
    [IDEE] FACTORY QUI DÉPEND D'UNE AUTRE FACTORY
    """
    created_posts = []
    
    def create_post(title, content="Default content", author=None):
        """Créer un post avec valeurs par défaut"""
        if author is None:
            # Créer un author par défaut
            author = user_factory("default_author")
        
        post = Post(title=title, content=content, author=author)
        post.save()
        created_posts.append(post)
        
        return post
    
    yield create_post
    
    # Cleanup
    for post in created_posts:
        post.delete()


def test_posts_with_authors(user_factory, post_factory):
    """
    [IDEE] UTILISER PLUSIEURS FACTORIES
    """
    # Créer authors
    alice = user_factory("alice")
    bob = user_factory("bob")
    
    # Créer posts avec différents authors
    post1 = post_factory("Post 1", author=alice)
    post2 = post_factory("Post 2", author=bob)
    post3 = post_factory("Post 3")  # Author par défaut
    
    assert post1.author == alice
    assert post2.author == bob
    assert post3.author is not None


"""
FACTORY FIXTURE AVANCÉE: AVEC BUILDER PATTERN
--------------------------------------------
"""

@pytest.fixture
def user_builder():
    """
    [IDEE] BUILDER PATTERN: Construire des objets complexes
    
    Permet de chaîner des méthodes pour configuration
    """
    class UserBuilder:
        def __init__(self):
            self.users = []
            self._username = "default"
            self._email = None
            self._age = 25
            self._active = True
        
        def with_username(self, username):
            """Chaînable: retourne self"""
            self._username = username
            return self
        
        def with_email(self, email):
            self._email = email
            return self
        
        def with_age(self, age):
            self._age = age
            return self
        
        def inactive(self):
            """Marquer comme inactif"""
            self._active = False
            return self
        
        def build(self):
            """Construire l'objet final"""
            if self._email is None:
                self._email = f"{self._username}@example.com"
            
            user = User(
                username=self._username,
                email=self._email,
                age=self._age,
                active=self._active
            )
            user.save()
            self.users.append(user)
            
            # Reset pour prochain build
            self.__init__()
            self.users = self.users  # Garder la liste
            
            return user
    
    builder = UserBuilder()
    yield builder
    
    # Cleanup
    for user in builder.users:
        user.delete()


def test_user_builder(user_builder):
    """
    [IDEE] UTILISATION DU BUILDER: Très lisible!
    """
    # Construire un user simple
    simple_user = user_builder.with_username("alice").build()
    
    # Construire un user complexe
    complex_user = (user_builder
                    .with_username("bob")
                    .with_email("custom@example.com")
                    .with_age(30)
                    .inactive()
                    .build())
    
    assert simple_user.username == "alice"
    assert simple_user.active is True
    
    assert complex_user.username == "bob"
    assert complex_user.email == "custom@example.com"
    assert complex_user.age == 30
    assert complex_user.active is False


"""
FINALIZERS: CLEANUP ALTERNATIF
-----------------------------

[IDEE] Alternative à yield pour le cleanup
"""

@pytest.fixture
def database_with_finalizer(request):
    """
    [IDEE] FINALIZER: Enregistrer une fonction de cleanup
    
    QUAND UTILISER?
    - Cleanup conditionnel
    - Plusieurs étapes de cleanup
    - Cleanup complexe
    """
    db = Database()
    db.connect()
    
    # Enregistrer le cleanup
    def cleanup():
        db.disconnect()
        print("\nDatabase disconnected")
    
    request.addfinalizer(cleanup)
    
    return db


@pytest.fixture
def complex_resource(request):
    """
    [IDEE] PLUSIEURS FINALIZERS
    
    Exécutés en ordre LIFO (dernier enregistré = premier exécuté)
    """
    resource = ComplexResource()
    
    # Finalizer 1
    def cleanup_step_1():
        print("\nStep 1: Save state")
        resource.save_state()
    
    # Finalizer 2
    def cleanup_step_2():
        print("\nStep 2: Close connections")
        resource.close_connections()
    
    # Finalizer 3
    def cleanup_step_3():
        print("\nStep 3: Delete resource")
        resource.delete()
    
    # Enregistrer dans l'ordre
    request.addfinalizer(cleanup_step_1)
    request.addfinalizer(cleanup_step_2)
    request.addfinalizer(cleanup_step_3)
    
    # Exécution: 3 -> 2 -> 1
    
    return resource


"""
[IDEE] YIELD VS FINALIZER:

YIELD (recommandé):
    [OK] Plus simple
    [OK] Plus pythonique
    [OK] Un seul point de cleanup

FINALIZER:
    [OK] Cleanup conditionnel
    [OK] Plusieurs étapes de cleanup
    [OK] Cleanup qui peut échouer partiellement


FIXTURES CONTEXTUELLES
---------------------

[IDEE] Fixtures qui s'adaptent selon le contexte du test
"""

@pytest.fixture
def api_mode(request):
    """
    [IDEE] FIXTURE CONTEXTUELLE
    
    Comportement différent selon le marker du test
    """
    # Vérifier si le test a un marker 'slow'
    if request.node.get_closest_marker('slow'):
        return "full"  # Mode complet pour tests lents
    else:
        return "fast"  # Mode rapide par défaut


@pytest.mark.slow
def test_full_api_call(api_mode):
    """Test avec mode full"""
    assert api_mode == "full"


def test_fast_api_call(api_mode):
    """Test avec mode fast"""
    assert api_mode == "fast"


"""
FIXTURES AVEC STATE PARTAGÉ (ATTENTION!)
---------------------------------------

[ATTENTION] PATTERN À ÉVITER (mais utile dans certains cas)
"""

@pytest.fixture(scope="module")
def shared_counter():
    """
    [ATTENTION] ATTENTION: State partagé entre tests!
    
    Peut causer des interdépendances
    """
    class Counter:
        def __init__(self):
            self.count = 0
        
        def increment(self):
            self.count += 1
        
        def reset(self):
            self.count = 0
    
    return Counter()


def test_counter_1(shared_counter):
    """Premier test"""
    shared_counter.increment()
    assert shared_counter.count == 1


def test_counter_2(shared_counter):
    """
    [ATTENTION] Deuxième test voit le state du premier!
    """
    shared_counter.increment()
    assert shared_counter.count == 2  # Pas 1!


"""
[IDEE] SI VRAIMENT NÉCESSAIRE:

Utilisez autouse pour reset:
"""

@pytest.fixture(autouse=True)
def reset_shared_counter(shared_counter):
    """Reset le counter avant chaque test"""
    yield
    shared_counter.reset()


"""
FIXTURES DYNAMIQUES
-----------------

[IDEE] Créer des fixtures à la volée
"""

def make_user_fixture(username):
    """
    [IDEE] FONCTION QUI CRÉE UNE FIXTURE
    
    Pattern avancé pour générer des fixtures dynamiquement
    """
    @pytest.fixture
    def user():
        user = User(username)
        user.save()
        yield user
        user.delete()
    
    return user


# Créer des fixtures spécifiques
alice_fixture = make_user_fixture("alice")
bob_fixture = make_user_fixture("bob")


"""
[ATTENTION] Rarement nécessaire, préférez factory fixtures!


[COURS] EXERCICE PRATIQUE #2: CRÉER UNE FACTORY FIXTURE COMPLÈTE
----------------------------------------------------------

MISSION:
Créer une factory fixture pour un système de blog:
- User factory
- Post factory (dépend de user)
- Comment factory (dépend de post et user)
- Avec cleanup automatique de tout
"""

@pytest.fixture
def blog_factory():
    """
    Factory complète pour système de blog
    
    VOTRE CODE ICI
    """
    pass


"""
SOLUTION:
"""

@pytest.fixture
def blog_factory():
    """
    [IDEE] FACTORY FIXTURE COMPLÈTE
    
    Gère users, posts, et comments avec cleanup
    """
    created_users = []
    created_posts = []
    created_comments = []
    
    class BlogFactory:
        def create_user(self, username, email=None):
            """Créer un user"""
            if email is None:
                email = f"{username}@example.com"
            
            user = User(username, email)
            user.save()
            created_users.append(user)
            return user
        
        def create_post(self, title, content="Default content", author=None):
            """Créer un post"""
            if author is None:
                author = self.create_user("default_author")
            
            post = Post(title=title, content=content, author=author)
            post.save()
            created_posts.append(post)
            return post
        
        def create_comment(self, post, text, author=None):
            """Créer un comment"""
            if author is None:
                author = self.create_user("default_commenter")
            
            comment = Comment(post=post, text=text, author=author)
            comment.save()
            created_comments.append(comment)
            return comment
    
    factory = BlogFactory()
    yield factory
    
    # Cleanup dans le bon ordre
    for comment in created_comments:
        comment.delete()
    for post in created_posts:
        post.delete()
    for user in created_users:
        user.delete()


def test_blog_system(blog_factory):
    """
    [IDEE] TEST COMPLET DU SYSTÈME DE BLOG
    """
    # Créer users
    alice = blog_factory.create_user("alice")
    bob = blog_factory.create_user("bob")
    
    # Créer posts
    post1 = blog_factory.create_post("Hello World", author=alice)
    post2 = blog_factory.create_post("Python Tips", author=bob)
    
    # Créer comments
    comment1 = blog_factory.create_comment(post1, "Great post!", author=bob)
    comment2 = blog_factory.create_comment(post1, "Thanks!", author=alice)
    comment3 = blog_factory.create_comment(post2, "Useful!", author=alice)
    
    # Vérifications
    assert len(post1.comments) == 2
    assert len(post2.comments) == 1
    assert comment1.author == bob
    
    # Tout sera nettoyé automatiquement!


"""
[DOCS] RÉCAPITULATIF DU CHAPITRE 8
-----------------------------

Vous avez appris:
[OK] Factory fixtures (retourner une fonction)
[OK] Builder pattern pour objets complexes
[OK] Finalizers (request.addfinalizer)
[OK] Fixtures contextuelles
[OK] State partagé (et pourquoi l'éviter)
[OK] Patterns avancés

Points clés:
[CLE] Factory fixture = retourne une fonction
[CLE] Permet de créer plusieurs instances
[CLE] Builder pattern = configuration fluide
[CLE] Finalizers = cleanup complexe
[CLE] Préférer yield aux finalizers


[OBJECTIF] QUAND UTILISER QUOI?

FIXTURE NORMALE:
    Une seule instance nécessaire

FACTORY FIXTURE:
    Plusieurs instances nécessaires
    Paramètres différents

BUILDER PATTERN:
    Objets très complexes
    Nombreuses options de configuration

FINALIZERS:
    Cleanup conditionnel
    Plusieurs étapes de cleanup


PROCHAINE PARTIE:
Parametrize et markers - Multiplier les tests!
"""

```

Voulez-vous continuer avec la Partie 6 (Parametrize, Markers et organisation) ?

# ============================================================================
# [LIVRE] PYTEST - GUIDE ULTRA-DÉTAILLÉ PARTIE 6
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE:
# - @pytest.mark.parametrize (tester avec plusieurs inputs)
# - Markers builtin (skip, xfail, etc.)
# - Markers personnalisés
# - Organisation et sélection de tests
# - Plugins pytest
#
# [TEMPS] TEMPS DE LECTURE: ~3-4 heures
# [DOCS] PRÉREQUIS: Avoir complété les Parties 1-5
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 9: PARAMETRIZE - MULTIPLIER LES TESTS
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Tester une fonction avec plusieurs jeux de données efficacement.


[REFLEXION] LE PROBLÈME: TESTS RÉPÉTITIFS
-------------------------------

Vous voulez tester une fonction avec plusieurs inputs:
"""

def add(a, b):
    """Additionner deux nombres"""
    return a + b


# [X] APPROCHE NAÏVE: Répéter le test
def test_add_positive():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, 1) == 0

def test_add_zero():
    assert add(0, 0) == 0

def test_add_large():
    assert add(1000, 2000) == 3000


"""
[X] PROBLÈMES:

1. DUPLICATION:
   - Même structure répétée 4 fois
   - Code verbeux

2. INCOMPLET:
   - Facile d'oublier des cas
   - Pas systématique

3. MAINTENANCE:
   - Modifier la structure = modifier 4 tests


[OK] SOLUTION: @pytest.mark.parametrize
------------------------------------

[IDEE] Un seul test, plusieurs jeux de données!
"""

import pytest

@pytest.mark.parametrize("a,b,expected", [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0),
    (1000, 2000, 3000),
])
def test_add(a, b, expected):
    """
    [IDEE] PARAMETRIZE: Un test, 4 exécutions!
    
    Le test s'exécute 4 fois avec différentes valeurs:
    - Test 1: a=2, b=3, expected=5
    - Test 2: a=-1, b=1, expected=0
    - Test 3: a=0, b=0, expected=0
    - Test 4: a=1000, b=2000, expected=3000
    """
    result = add(a, b)
    assert result == expected


"""
[IDEE] EXÉCUTION:

pytest -v

Output:
test_add[2-3-5] PASSED
test_add[-1-1-0] PASSED
test_add[0-0-0] PASSED
test_add[1000-2000-3000] PASSED

4 tests exécutés avec un seul def test_!


[REFLEXION] DÉCORTIQUONS LA SYNTAXE:

@pytest.mark.parametrize(
    "a,b,expected",           <- Noms des paramètres (séparés par virgules)
    [                         <- Liste de tuples
        (2, 3, 5),           <- Tuple 1: a=2, b=3, expected=5
        (-1, 1, 0),          <- Tuple 2: a=-1, b=1, expected=0
        (0, 0, 0),           <- Tuple 3
        (1000, 2000, 3000),  <- Tuple 4
    ]
)


PARAMETRIZE AVEC UN SEUL PARAMÈTRE
---------------------------------
"""

@pytest.mark.parametrize("number", [1, 2, 3, 4, 5])
def test_positive_numbers(number):
    """
    [IDEE] UN SEUL PARAMÈTRE
    
    Liste simple (pas de tuples nécessaires)
    """
    assert number > 0


@pytest.mark.parametrize("value", [
    "",           # String vide
    [],           # Liste vide
    {},           # Dict vide
    None,         # None
    0,            # Zéro
])
def test_falsy_values(value):
    """Tester toutes les valeurs falsy"""
    assert not value


"""
PARAMETRIZE AVEC NOMS PERSONNALISÉS (IDS)
----------------------------------------

Par défaut, pytest affiche les valeurs: test_add[2-3-5]
Vous pouvez personnaliser avec ids:
"""

@pytest.mark.parametrize("username,email,valid", [
    ("alice", "alice@example.com", True),
    ("bob123", "bob@example.com", True),
    ("a", "a@example.com", False),  # Trop court
    ("user", "invalid-email", False),
], ids=["valid-alice", "valid-bob", "short-username", "invalid-email"])
def test_user_validation(username, email, valid):
    """
    [IDEE] IDS PERSONNALISÉS
    
    Plus lisible que les valeurs brutes!
    """
    is_valid = validate_user(username, email)
    assert is_valid == valid


"""
Output:
test_user_validation[valid-alice] PASSED
test_user_validation[valid-bob] PASSED
test_user_validation[short-username] PASSED
test_user_validation[invalid-email] PASSED


IDS AVEC FONCTION
----------------
"""

def user_id(param):
    """
    [IDEE] FONCTION POUR GÉNÉRER IDS DYNAMIQUEMENT
    
    param = le tuple complet
    """
    username, email, valid = param
    status = "valid" if valid else "invalid"
    return f"{username}-{status}"


@pytest.mark.parametrize("username,email,valid", [
    ("alice", "alice@example.com", True),
    ("bob123", "bob@example.com", True),
    ("a", "a@example.com", False),
], ids=user_id)
def test_user_with_dynamic_ids(username, email, valid):
    is_valid = validate_user(username, email)
    assert is_valid == valid


"""
Output:
test_user_with_dynamic_ids[alice-valid] PASSED
test_user_with_dynamic_ids[bob123-valid] PASSED
test_user_with_dynamic_ids[a-invalid] PASSED


PARAMETRIZE MULTIPLE
-------------------

[IDEE] Plusieurs @pytest.mark.parametrize = PRODUIT CARTÉSIEN!
"""

@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [3, 4])
def test_multiplication(x, y):
    """
    [IDEE] PRODUIT CARTÉSIEN!
    
    2 valeurs de x × 2 valeurs de y = 4 tests:
    - x=1, y=3
    - x=1, y=4
    - x=2, y=3
    - x=2, y=4
    """
    result = x * y
    assert result > 0


"""
Output:
test_multiplication[1-3] PASSED
test_multiplication[1-4] PASSED
test_multiplication[2-3] PASSED
test_multiplication[2-4] PASSED


[ATTENTION] ATTENTION À L'EXPLOSION COMBINATOIRE!

3 parametrize × 3 valeurs chacun = 27 tests
4 parametrize × 5 valeurs = 625 tests!


PARAMETRIZE AVEC pytest.param()
------------------------------

[IDEE] pytest.param() pour options avancées par cas de test
"""

@pytest.mark.parametrize("x,y,expected", [
    (2, 3, 5),
    (10, 20, 30),
    pytest.param(100, 200, 300, marks=pytest.mark.slow),  # <- Marquer ce cas
    pytest.param(1, 1, 3, marks=pytest.mark.xfail),       # <- Expected to fail
])
def test_add_advanced(x, y, expected):
    """
    [IDEE] pytest.param() pour options par cas
    
    - Markers spécifiques
    - IDs personnalisés
    - Autres options
    """
    assert x + y == expected


"""
pytest.param(
    values,              # Valeurs (comme un tuple)
    marks=marker,        # Marker pour ce cas
    id="custom-id"       # ID personnalisé
)


PARAMETRIZE AVEC OBJETS COMPLEXES
--------------------------------
"""

@pytest.mark.parametrize("user_data", [
    {"username": "alice", "age": 25, "active": True},
    {"username": "bob", "age": 30, "active": False},
    {"username": "charlie", "age": 35, "active": True},
])
def test_user_creation(user_data):
    """Parametrize avec dicts"""
    user = User(**user_data)
    assert user.username == user_data["username"]
    assert user.age == user_data["age"]


@pytest.mark.parametrize("config", [
    {"db": "sqlite", "cache": True},
    {"db": "postgres", "cache": False},
], ids=["sqlite-cached", "postgres-uncached"])
def test_with_config(config):
    """Parametrize avec configuration"""
    app = create_app(config)
    assert app.database == config["db"]


"""
PARAMETRIZE INDIRECT
------------------

[IDEE] Passer les paramètres à une fixture au lieu du test directement
"""

@pytest.fixture
def user(request):
    """
    Fixture qui reçoit le paramètre
    """
    username = request.param
    user = User(username)
    user.save()
    yield user
    user.delete()


@pytest.mark.parametrize("user", ["alice", "bob", "charlie"], indirect=True)
def test_user_exists(user):
    """
    [IDEE] INDIRECT = TRUE
    
    Les valeurs sont passées à la fixture user,
    pas directement au test!
    
    Permet d'avoir du setup/teardown pour chaque valeur
    """
    assert user.id is not None
    assert user.username in ["alice", "bob", "charlie"]


"""
[IDEE] INDIRECT PARTIEL:

Certains params indirect, d'autres non:
"""

@pytest.fixture
def db(request):
    db_type = request.param
    db = create_database(db_type)
    yield db
    db.close()


@pytest.mark.parametrize("db,query", [
    ("sqlite", "SELECT 1"),
    ("postgres", "SELECT 1"),
], indirect=["db"])  # <- Seulement 'db' est indirect
def test_query(db, query):
    """
    db: passé à la fixture
    query: passé directement au test
    """
    result = db.execute(query)
    assert result is not None


"""
LIRE PARAMETRIZE DEPUIS UN FICHIER
---------------------------------

[IDEE] Pour nombreux cas de test, charger depuis CSV/JSON
"""

import json

def load_test_data():
    """Charger données de test depuis JSON"""
    with open("test_data.json") as f:
        return json.load(f)


@pytest.mark.parametrize("test_case", load_test_data())
def test_from_file(test_case):
    """
    [IDEE] DONNÉES DEPUIS FICHIER
    
    Pratique pour:
    - Nombreux cas de test
    - Partager données entre équipes
    - Générer données programmatiquement
    """
    input_val = test_case["input"]
    expected = test_case["expected"]
    result = my_function(input_val)
    assert result == expected


"""
test_data.json:
[
    {"input": 1, "expected": 2},
    {"input": 2, "expected": 4},
    {"input": 3, "expected": 6}
]


[COURS] EXERCICE PRATIQUE #1: PARAMETRIZE COMPLET
-------------------------------------------

MISSION:
Tester une fonction de validation d'email avec parametrize
"""

def is_valid_email(email):
    """Valider un email (simplifié)"""
    if not email:
        return False
    if "@" not in email:
        return False
    if "." not in email.split("@")[1]:
        return False
    return True


"""
Testez avec parametrize:
- Emails valides: alice@example.com, bob@test.co.uk
- Emails invalides: 
  - Vide: ""
  - Sans @: "invalid"
  - Sans domaine: "user@"
  - Sans extension: "user@domain"

SOLUTION:
"""

@pytest.mark.parametrize("email,expected,reason", [
    # Valides
    ("alice@example.com", True, "standard"),
    ("bob@test.co.uk", True, "multiple-dots"),
    ("user+tag@domain.com", True, "with-plus"),
    
    # Invalides
    ("", False, "empty"),
    ("invalid", False, "no-at"),
    ("user@", False, "no-domain"),
    ("user@domain", False, "no-extension"),
    ("@domain.com", False, "no-username"),
], ids=lambda x: x[2])  # Utiliser 'reason' comme ID
def test_email_validation(email, expected, reason):
    """
    [IDEE] TEST COMPLET DE VALIDATION EMAIL
    
    Couvre tous les cas avec un seul test!
    """
    result = is_valid_email(email)
    assert result == expected, f"Failed for {reason}: {email}"


"""
PARAMETRIZE AVEC FIXTURES
------------------------

Combiner parametrize et fixtures:
"""

@pytest.fixture
def database():
    """Fixture classique"""
    db = Database()
    db.connect()
    yield db
    db.disconnect()


@pytest.mark.parametrize("table_name", ["users", "posts", "comments"])
def test_table_exists(database, table_name):
    """
    [IDEE] COMBINER FIXTURE ET PARAMETRIZE
    
    database: fixture normale
    table_name: paramétré
    
    3 tests exécutés, chacun avec une connexion DB
    """
    assert database.table_exists(table_name)


"""
PARAMETRIZE AU NIVEAU CLASSE
---------------------------
"""

@pytest.mark.parametrize("value", [1, 2, 3])
class TestNumbers:
    """
    [IDEE] PARAMETRIZE SUR UNE CLASSE
    
    Appliqué à TOUS les tests de la classe!
    """
    
    def test_positive(self, value):
        """S'exécute 3 fois"""
        assert value > 0
    
    def test_type(self, value):
        """S'exécute 3 fois aussi"""
        assert isinstance(value, int)


"""
Output:
TestNumbers::test_positive[1] PASSED
TestNumbers::test_positive[2] PASSED
TestNumbers::test_positive[3] PASSED
TestNumbers::test_type[1] PASSED
TestNumbers::test_type[2] PASSED
TestNumbers::test_type[3] PASSED

6 tests au total (2 méthodes × 3 valeurs)


[DOCS] RÉCAPITULATIF PARAMETRIZE
---------------------------

Vous avez appris:
[OK] @pytest.mark.parametrize pour multiplier les tests
[OK] Syntaxe: parametrize("params", [values])
[OK] ids pour noms personnalisés
[OK] Parametrize multiple = produit cartésien
[OK] pytest.param() pour options avancées
[OK] indirect=True pour passer à fixtures
[OK] Charger données depuis fichier

Points clés:
[CLE] Un test, plusieurs exécutions
[CLE] Format: ("param1,param2", [(val1, val2), ...])
[CLE] ids= pour tests lisibles
[CLE] Attention produit cartésien!
[CLE] indirect pour fixtures


[OBJECTIF] QUAND UTILISER PARAMETRIZE?

[OK] Tester fonction avec différents inputs
[OK] Cas valides et invalides
[OK] Edge cases systématiques
[OK] Tests de régression


PROCHAIN CHAPITRE:
Markers - Organiser et sélectionner les tests!
"""


# ============================================================================
# [GUIDE] CHAPITRE 10: MARKERS - ORGANISER LES TESTS
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Utiliser les markers pour organiser et contrôler l'exécution des tests.


[REFLEXION] QU'EST-CE QU'UN MARKER?
-------------------------

MARKER = Tag/étiquette pour marquer un test

[IDEE] Comme des labels qui disent:
- "Ce test est lent"
- "Ce test nécessite la DB"
- "Ce test est en développement"
- "Ce test est un test d'intégration"

UTILITÉ:
[OK] Organiser les tests par catégorie
[OK] Exécuter seulement certains tests
[OK] Sauter des tests selon conditions
[OK] Documenter les tests


MARKERS BUILTIN DE PYTEST
------------------------

pytest fournit des markers prêts à l'emploi:

1. skip : Sauter un test
2. skipif : Sauter si condition
3. xfail : Expected to fail
4. parametrize : Vu au chapitre précédent
5. filterwarnings : Filtrer warnings
6. timeout : Limite de temps


MARKER: SKIP
-----------
"""

import pytest

@pytest.mark.skip
def test_not_ready():
    """
    [IDEE] SKIP: Sauter ce test
    
    QUAND UTILISER?
    - Feature pas encore implémentée
    - Test temporairement cassé
    - Test à refactoriser
    """
    # Ce test n'est jamais exécuté
    assert False


@pytest.mark.skip(reason="Waiting for API v2")
def test_api_v2():
    """
    [IDEE] SKIP AVEC RAISON
    
    La raison s'affiche dans le rapport!
    """
    # Pas exécuté
    call_api_v2()


"""
Output:
test_not_ready SKIPPED
test_api_v2 SKIPPED (Waiting for API v2)


SKIP CONDITIONNEL (pytest.skipif)
--------------------------------
"""

import sys

@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python 3.8+")
def test_walrus_operator():
    """
    [IDEE] SKIPIF: Sauter si condition vraie
    
    Utile pour:
    - Versions Python
    - OS spécifiques
    - Dépendances optionnelles
    """
    # Walrus operator := (Python 3.8+)
    if (n := 10) > 5:
        assert n == 10


@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
def test_unix_specific():
    """Sauté sur Windows"""
    import pwd  # Module Unix
    assert pwd.getpwuid(0)


"""
SKIP DEPUIS LE TEST
------------------
"""

def test_dynamic_skip():
    """
    [IDEE] SKIP PENDANT L'EXÉCUTION
    
    Décider de sauter selon des conditions runtime
    """
    if not has_database_connection():
        pytest.skip("Database not available")
    
    # Suite du test seulement si DB disponible
    result = query_database()
    assert result is not None


"""
MARKER: XFAIL (Expected Fail)
----------------------------
"""

@pytest.mark.xfail
def test_known_bug():
    """
    [IDEE] XFAIL: Ce test devrait échouer
    
    QUAND UTILISER?
    - Bug connu pas encore fixé
    - Feature partiellement implémentée
    - Comportement non déterministe
    
    Si le test:
    - Échoue -> Marqué XFAIL (attendu)
    - Passe -> Marqué XPASS (surprise!)
    """
    # On sait que ça ne marche pas encore
    result = buggy_function()
    assert result == "expected"


@pytest.mark.xfail(reason="Bug #123: Division by zero not handled")
def test_division_by_zero():
    """XFAIL avec raison"""
    divide(10, 0)  # Devrait lever exception mais ne le fait pas


@pytest.mark.xfail(strict=True)
def test_strict_xfail():
    """
    [IDEE] STRICT XFAIL
    
    Si le test passe -> Le test ÉCHOUE!
    
    QUAND UTILISER?
    - Vous voulez être notifié quand le bug est fixé
    """
    assert buggy_function() == "expected"


"""
XFAIL VS SKIP:

SKIP:
    "On n'exécute pas ce test"
    Compte: X skipped

XFAIL:
    "On exécute, mais on s'attend à un échec"
    Si échoue: X xfailed (attendu)
    Si passe: X xpassed (surprise!)


XFAIL CONDITIONNEL
-----------------
"""

@pytest.mark.xfail(sys.platform == "win32", reason="Fails on Windows")
def test_platform_specific():
    """XFAIL seulement sur Windows"""
    assert unix_specific_function()


"""
CRÉER DES MARKERS PERSONNALISÉS
------------------------------

[IDEE] Créer vos propres markers pour organiser les tests
"""

# pytest.ini ou pyproject.toml
"""
[pytest]
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    integration: marks tests as integration tests
    unit: marks tests as unit tests
    smoke: marks tests as smoke tests
    api: marks tests that call external APIs
"""


# Utiliser les markers personnalisés
@pytest.mark.slow
def test_complex_computation():
    """
    [IDEE] MARKER PERSONNALISÉ: slow
    
    Marquer les tests lents pour pouvoir les sauter
    """
    result = very_slow_function()  # Prend 10 secondes
    assert result is not None


@pytest.mark.integration
def test_database_integration():
    """Marker: integration"""
    db = connect_to_database()
    result = db.query("SELECT 1")
    assert result


@pytest.mark.unit
def test_pure_function():
    """Marker: unit"""
    assert add(2, 3) == 5


@pytest.mark.smoke
def test_app_starts():
    """
    [IDEE] SMOKE TEST
    
    Test rapide pour vérifier que l'app fonctionne basiquement
    """
    app = create_app()
    assert app.is_running()


"""
EXÉCUTER SELON LES MARKERS
-------------------------

Lancer seulement certains tests:
"""

# Seulement les tests "slow"
pytest -m slow

# Seulement les tests "integration"
pytest -m integration

# Tous SAUF les tests "slow"
pytest -m "not slow"

# Tests "unit" OU "integration"
pytest -m "unit or integration"

# Tests "smoke" ET "api"
pytest -m "smoke and api"

# Tests "integration" mais PAS "slow"
pytest -m "integration and not slow"


"""
[IDEE] EXPRESSIONS BOOLÉENNES:

and : Les deux markers
or  : Au moins un marker
not : Pas ce marker
()  : Grouper


COMBINER PLUSIEURS MARKERS
-------------------------
"""

@pytest.mark.slow
@pytest.mark.integration
@pytest.mark.api
def test_full_api_flow():
    """
    [IDEE] PLUSIEURS MARKERS
    
    Ce test a 3 markers!
    """
    # Test complet qui prend du temps
    result = call_external_api()
    save_to_database(result)
    assert verify_in_database(result.id)


"""
Sélectionner ce test:
pytest -m slow           [OK]
pytest -m integration    [OK]
pytest -m api            [OK]
pytest -m "slow and api" [OK]
pytest -m unit           [X]


MARKERS AVEC PARAMÈTRES
----------------------
"""

@pytest.mark.timeout(5)  # Timeout de 5 secondes
def test_with_timeout():
    """
    [IDEE] MARKER AVEC PARAMÈTRE
    
    Nécessite: pip install pytest-timeout
    """
    slow_operation()


@pytest.mark.repeat(3)  # Répéter 3 fois
def test_flaky():
    """
    Nécessite: pip install pytest-repeat
    
    Utile pour tests non déterministes
    """
    assert random_function()


"""
MARKERS AU NIVEAU CLASSE
-----------------------
"""

@pytest.mark.integration
class TestDatabaseOperations:
    """
    [IDEE] MARKER SUR CLASSE
    
    Appliqué à TOUS les tests de la classe!
    """
    
    def test_insert(self):
        """A le marker 'integration'"""
        pass
    
    def test_update(self):
        """A le marker 'integration' aussi"""
        pass
    
    @pytest.mark.slow
    def test_bulk_insert(self):
        """
        A les markers 'integration' ET 'slow'!
        """
        pass


"""
MARKERS AU NIVEAU MODULE
-----------------------
"""

# En haut du fichier
pytestmark = pytest.mark.integration

"""
Tous les tests de ce fichier auront le marker 'integration'!
"""

def test_1():
    """A 'integration'"""
    pass

def test_2():
    """A 'integration'"""
    pass


"""
MULTIPLE MARKERS AU NIVEAU MODULE:
"""

pytestmark = [pytest.mark.integration, pytest.mark.slow]


"""
ACCÉDER AUX MARKERS DANS LES TESTS
---------------------------------
"""

def test_check_markers(request):
    """
    [IDEE] ACCÉDER AUX MARKERS DU TEST
    
    Via request.node.iter_markers()
    """
    markers = [m.name for m in request.node.iter_markers()]
    print(f"\nThis test has markers: {markers}")


"""
[COURS] EXERCICE PRATIQUE #2: ORGANISER AVEC MARKERS
----------------------------------------------

MISSION:
Organiser une suite de tests avec markers appropriés
"""

# pytest.ini
"""
[pytest]
markers =
    unit: Unit tests
    integration: Integration tests
    slow: Slow tests (>1s)
    db: Tests requiring database
    api: Tests calling external APIs
    smoke: Smoke tests
"""


# Tests à organiser
@pytest.mark.unit
def test_calculation():
    """Test unitaire rapide"""
    assert 1 + 1 == 2


@pytest.mark.integration
@pytest.mark.db
def test_user_crud():
    """Test d'intégration avec DB"""
    user = create_user("alice")
    assert get_user(user.id).username == "alice"


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.slow
def test_external_api():
    """Test API externe (lent)"""
    result = call_weather_api("Paris")
    assert result["city"] == "Paris"


@pytest.mark.smoke
def test_app_health():
    """Smoke test: app démarre"""
    app = create_app()
    assert app.is_healthy()


"""
COMMANDES UTILES:

# Tests rapides (smoke + unit)
pytest -m "smoke or unit"

# Tests sans DB ni API
pytest -m "not db and not api"

# Tests d'intégration rapides
pytest -m "integration and not slow"

# Tous les tests DB
pytest -m db


MARKERS POUR CI/CD
-----------------

[IDEE] Pattern pour CI/CD avec différents stages:
"""

# Stage 1: Tests rapides (< 1s)
# pytest -m "not slow"

# Stage 2: Tests d'intégration
# pytest -m integration

# Stage 3: Tests lents complets
# pytest -m slow

# Stage 4: Smoke tests en production
# pytest -m smoke


"""
[DOCS] RÉCAPITULATIF DU CHAPITRE 10
------------------------------

Vous avez appris:
[OK] Markers builtin (skip, skipif, xfail)
[OK] Créer markers personnalisés
[OK] Sélectionner tests avec -m
[OK] Combiner markers
[OK] Markers au niveau classe/module
[OK] Organisation pour CI/CD

Points clés:
[CLE] Markers = tags pour organiser tests
[CLE] skip = ne pas exécuter
[CLE] xfail = échec attendu
[CLE] -m "expression" pour sélectionner
[CLE] Déclarer markers dans pytest.ini


[OBJECTIF] STRATÉGIE D'ORGANISATION:

MARKERS RECOMMANDÉS:
- unit: Tests unitaires
- integration: Tests d'intégration
- slow: Tests > 1 seconde
- smoke: Tests critiques rapides
- db, api, external: Par dépendance

CI/CD STAGES:
1. Smoke tests (30s)
2. Unit tests (2 min)
3. Integration tests (5 min)
4. Full suite (15 min)


PROCHAIN CHAPITRE:
Plugins et configuration avancée!
"""

# ============================================================================
# [LIVRE] PYTEST - GUIDE ULTRA-DÉTAILLÉ PARTIE 7 (FINALE)
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE:
# - Configuration pytest (pytest.ini, pyproject.toml, setup.cfg)
# - Plugins pytest essentiels
# - Hooks pytest
# - Rapports et coverage
# - Best practices et patterns
# - Debugging de tests
#
# [TEMPS] TEMPS DE LECTURE: ~3-4 heures
# [DOCS] PRÉREQUIS: Avoir complété les Parties 1-6
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 11: CONFIGURATION PYTEST
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Maîtriser la configuration de pytest pour des projets professionnels.


[REFLEXION] FICHIERS DE CONFIGURATION
---------------------------

Pytest peut être configuré via plusieurs fichiers:

1. pytest.ini (recommandé, spécifique pytest)
2. pyproject.toml (moderne, pour tout le projet)
3. setup.cfg (ancien style)
4. tox.ini (si vous utilisez tox)


PYTEST.INI: CONFIGURATION STANDARD
---------------------------------
"""

# pytest.ini
"""
[IDEE] PYTEST.INI: Fichier de configuration principal

À placer à la racine du projet
"""

[pytest]
# === DÉCOUVERTE DES TESTS ===
# Où chercher les tests
testpaths = tests

# Patterns de fichiers
python_files = test_*.py *_test.py

# Patterns de classes
python_classes = Test*

# Patterns de fonctions
python_functions = test_*


# === OPTIONS PAR DÉFAUT ===
# Options CLI appliquées automatiquement
addopts = 
    -v                  # Verbose
    --strict-markers    # Erreur si marker non déclaré
    --tb=short         # Traceback court
    -ra                # Résumé de tous les tests
    --cov=myapp        # Coverage
    --cov-report=html  # Rapport HTML
    --cov-report=term  # Rapport terminal


# === MARKERS PERSONNALISÉS ===
markers =
    slow: Tests lents (>1s)
    integration: Tests d'intégration
    unit: Tests unitaires
    smoke: Tests de fumée
    db: Tests nécessitant une base de données
    api: Tests appelant des APIs externes
    wip: Work in progress


# === TIMEOUT ===
# Timeout global pour tous les tests
# (nécessite pytest-timeout)
timeout = 300


# === WARNINGS ===
# Filtrer les warnings
filterwarnings =
    error                # Traiter warnings comme erreurs
    ignore::UserWarning  # Ignorer UserWarnings
    ignore::DeprecationWarning:mylib.*  # Ignorer dans mylib


# === CHEMINS PYTHON ===
# Ajouter des chemins à sys.path
pythonpath = src


# === MIN/MAX VERSION ===
minversion = 7.0
# pytest refusera de s'exécuter si version < 7.0


# === CONSIDÉRER COMME FICHIERS RACINE ===
# Empêcher pytest de remonter plus haut
# (utile pour monorepos)
# rootdir = .


"""
[IDEE] DÉCORTIQUONS CHAQUE SECTION:

TESTPATHS:
    Où pytest cherche les tests
    Accélère la découverte

ADDOPTS:
    Options CLI par défaut
    Évite de les taper à chaque fois

MARKERS:
    Déclarer tous vos markers
    --strict-markers échouera si marker non déclaré

FILTERWARNINGS:
    Contrôler les warnings
    error = traiter comme erreurs
    ignore = ignorer certains


PYPROJECT.TOML: CONFIGURATION MODERNE
------------------------------------
"""

# pyproject.toml
"""
[IDEE] PYPROJECT.TOML: Configuration Python moderne

Tout le projet dans un fichier!
"""

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]

addopts = [
    "-v",
    "--strict-markers",
    "--tb=short",
    "-ra",
]

markers = [
    "slow: marks tests as slow",
    "integration: marks integration tests",
    "unit: marks unit tests",
]

# Autres outils dans le même fichier
[tool.black]
line-length = 88

[tool.mypy]
python_version = "3.11"
warn_return_any = true


"""
[IDEE] AVANTAGE:
Un seul fichier pour tout:
- pytest
- black (formatter)
- mypy (type checker)
- poetry (gestionnaire de dépendances)
- etc.


CONFIGURATION PAR ENVIRONNEMENT
------------------------------

[IDEE] Différentes configs selon l'environnement:
"""

# conftest.py
import pytest
import os

def pytest_configure(config):
    """
    [IDEE] HOOK: Configuration dynamique
    
    Appelé au début de pytest
    """
    # Détecter environnement
    env = os.getenv("TEST_ENV", "local")
    
    if env == "ci":
        # Configuration pour CI/CD
        config.option.verbose = 2
        config.option.numprocesses = 4  # Parallélisation
    elif env == "local":
        # Configuration locale
        config.option.verbose = 1


"""
Utilisation:
"""

# Local
pytest

# CI/CD
TEST_ENV=ci pytest


"""
CONFIGURATION AVANCÉE
--------------------
"""

# pytest.ini
[pytest]
# === COLLECTE DE TESTS ===
# Ne pas descendre dans ces dossiers
norecursedirs = .* build dist *.egg venv


# === CONSOLE OUTPUT ===
# Largeur de la console
console_output_style = progress  # ou classic, count


# === LOG CAPTURE ===
# Niveau de log capturé
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)s] %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S

# Logs dans fichier
log_file = tests.log
log_file_level = DEBUG
log_file_format = %(asctime)s [%(levelname)s] %(message)s


# === VARIABLES D'ENVIRONNEMENT ===
# Définir des variables pour les tests
env =
    TESTING=1
    DATABASE_URL=sqlite:///:memory:


"""
[COURS] EXERCICE PRATIQUE #1: CONFIGURER UN PROJET
--------------------------------------------

MISSION:
Créer une configuration complète pour un projet FastAPI
"""

# pytest.ini
[pytest]
# VOTRE CONFIGURATION ICI

"""
Inclure:
1. Découverte de tests (dossier tests/)
2. Options verbose et coverage
3. Markers: unit, integration, api, slow
4. Timeout de 300s
5. Ignorer les DeprecationWarnings
6. Logs en mode INFO


SOLUTION:
"""

# pytest.ini
[pytest]
# Découverte
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*

# Options par défaut
addopts =
    -v
    -ra
    --strict-markers
    --tb=short
    --cov=app
    --cov-report=html
    --cov-report=term-missing
    --cov-fail-under=80

# Markers
markers =
    unit: Unit tests
    integration: Integration tests
    api: API tests
    slow: Slow tests (>1s)
    smoke: Smoke tests

# Timeout
timeout = 300

# Warnings
filterwarnings =
    ignore::DeprecationWarning

# Logs
log_cli = true
log_cli_level = INFO

# Python path
pythonpath = src


"""
[DOCS] RÉCAPITULATIF CONFIGURATION
-----------------------------

Vous avez appris:
[OK] pytest.ini pour configuration
[OK] pyproject.toml pour projets modernes
[OK] Sections importantes (testpaths, addopts, markers)
[OK] Configuration par environnement
[OK] Logs et warnings

Points clés:
[CLE] pytest.ini à la racine
[CLE] addopts = options CLI par défaut
[CLE] Déclarer tous les markers
[CLE] --strict-markers pour sécurité


PROCHAIN CHAPITRE:
Plugins pytest essentiels!
"""


# ============================================================================
# [GUIDE] CHAPITRE 12: PLUGINS PYTEST ESSENTIELS
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Découvrir et utiliser les plugins pytest les plus utiles.


[REFLEXION] QU'EST-CE QU'UN PLUGIN PYTEST?
--------------------------------

Plugin = Extension qui ajoute des fonctionnalités à pytest

[IDEE] Pytest est TRÈS extensible!

Plus de 1000 plugins disponibles:
https://docs.pytest.org/en/latest/reference/plugin_list.html


INSTALLER DES PLUGINS
--------------------
"""

# Plugins individuels
pip install pytest-cov
pip install pytest-xdist
pip install pytest-timeout

# Ou plusieurs en une fois
pip install pytest-cov pytest-xdist pytest-timeout pytest-mock


"""
PLUGIN 1: PYTEST-COV (Coverage)
------------------------------

[IDEE] Mesurer la couverture de code par les tests
"""

pip install pytest-cov

# Lancer avec coverage
pytest --cov=myapp

# Avec rapport HTML
pytest --cov=myapp --cov-report=html

# Avec rapport XML (pour CI)
pytest --cov=myapp --cov-report=xml

# Voir les lignes manquantes
pytest --cov=myapp --cov-report=term-missing

# Échouer si coverage < 80%
pytest --cov=myapp --cov-fail-under=80


"""
[IDEE] CONFIGURATION DANS pytest.ini:
"""

[pytest]
addopts = 
    --cov=myapp
    --cov-report=html
    --cov-report=term-missing
    --cov-fail-under=80


"""
[IDEE] EXCLURE DES FICHIERS:
"""

# .coveragerc ou pyproject.toml
[coverage:run]
omit = 
    */tests/*
    */migrations/*
    */venv/*
    */__init__.py


"""
PLUGIN 2: PYTEST-XDIST (Parallélisation)
---------------------------------------

[IDEE] Exécuter les tests en parallèle!
"""

pip install pytest-xdist

# Paralléliser sur tous les CPU
pytest -n auto

# Paralléliser sur 4 workers
pytest -n 4

# Distribution sur plusieurs machines (avancé)
pytest -d --tx ssh=user@host


"""
[IDEE] GAIN DE PERFORMANCE:

100 tests × 1s chacun = 100s

Avec 4 workers:
100 tests / 4 workers = 25s

4× plus rapide!


[ATTENTION] ATTENTION:
- Tests doivent être indépendants
- Fixtures de scope session ne partagent pas entre workers
- Ordre des tests non garanti


PLUGIN 3: PYTEST-TIMEOUT
-----------------------

[IDEE] Timeout automatique pour éviter tests bloqués
"""

pip install pytest-timeout

# Timeout global (tous les tests)
pytest --timeout=300

# Timeout par test
@pytest.mark.timeout(10)
def test_slow_operation():
    """Max 10 secondes"""
    slow_function()


# Timeout avec méthode
@pytest.mark.timeout(5, method="thread")  # ou "signal"
def test_with_timeout():
    pass


"""
Configuration:
"""

[pytest]
timeout = 300  # 5 minutes par défaut


"""
PLUGIN 4: PYTEST-MOCK
--------------------

[IDEE] Mocking simplifié avec pytest
"""

pip install pytest-mock

def test_with_mock(mocker):
    """
    [IDEE] FIXTURE MOCKER
    
    Simplifie l'utilisation de unittest.mock
    """
    # Mock une fonction
    mock_func = mocker.patch("myapp.api.call_external")
    mock_func.return_value = {"status": "ok"}
    
    # Appeler
    result = my_function()
    
    # Vérifier
    assert result == {"status": "ok"}
    mock_func.assert_called_once()


def test_mock_object_method(mocker):
    """Mocker une méthode d'objet"""
    obj = MyClass()
    
    # Mock la méthode
    mocker.patch.object(obj, "method", return_value=42)
    
    assert obj.method() == 42


"""
PLUGIN 5: PYTEST-DJANGO
----------------------

[IDEE] Pour tester des applications Django
"""

pip install pytest-django

# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = test_*.py


# Tests Django
@pytest.mark.django_db
def test_user_creation(db):
    """
    [IDEE] MARKER django_db: Accès à la DB
    """
    from myapp.models import User
    
    user = User.objects.create(username="alice")
    assert User.objects.count() == 1


def test_client_get(client):
    """
    [IDEE] FIXTURE client: Django test client
    """
    response = client.get("/")
    assert response.status_code == 200


"""
PLUGIN 6: PYTEST-ASYNCIO
-----------------------

[IDEE] Pour tester du code asynchrone
"""

pip install pytest-asyncio

@pytest.mark.asyncio
async def test_async_function():
    """
    [IDEE] MARKER asyncio: Test async
    """
    result = await async_function()
    assert result == "expected"


@pytest.fixture
async def async_fixture():
    """Fixture async"""
    resource = await create_resource()
    yield resource
    await cleanup(resource)


"""
PLUGIN 7: PYTEST-BDD
-------------------

[IDEE] Behavior-Driven Development
"""

pip install pytest-bdd

# features/user.feature
"""
Feature: User management
    
    Scenario: Create a user
        Given I have user data
        When I create a user
        Then the user exists in database
"""


# test_user.py
from pytest_bdd import scenarios, given, when, then

scenarios("features/user.feature")

@given("I have user data")
def user_data():
    return {"username": "alice"}

@when("I create a user")
def create_user(user_data):
    return User.objects.create(**user_data)

@then("the user exists in database")
def user_exists(create_user):
    assert User.objects.filter(id=create_user.id).exists()


"""
PLUGIN 8: PYTEST-BENCHMARK
-------------------------

[IDEE] Benchmarking de performance
"""

pip install pytest-benchmark

def test_function_performance(benchmark):
    """
    [IDEE] FIXTURE benchmark: Mesurer performance
    """
    result = benchmark(my_function, arg1, arg2)
    assert result == expected


# Avec setup
def test_with_setup(benchmark):
    def setup():
        return prepare_data()
    
    benchmark.pedantic(my_function, setup=setup, rounds=100)


"""
PLUGIN 9: PYTEST-HTML
--------------------

[IDEE] Rapports HTML élégants
"""

pip install pytest-html

# Générer rapport
pytest --html=report.html

# Avec self-contained (tout dans un fichier)
pytest --html=report.html --self-contained-html


"""
PLUGIN 10: PYTEST-SUGAR
----------------------

[IDEE] Output plus joli et lisible
"""

pip install pytest-sugar

# Pas de configuration nécessaire!
# pytest affichera automatiquement avec des couleurs et animations


"""
CRÉER VOTRE PROPRE PLUGIN
------------------------

[IDEE] Plugins simples dans conftest.py
"""

# conftest.py
import pytest

def pytest_addoption(parser):
    """
    [IDEE] HOOK: Ajouter options CLI
    """
    parser.addoption(
        "--env",
        action="store",
        default="local",
        help="Environment: local, staging, prod"
    )


@pytest.fixture
def env(request):
    """Fixture pour récupérer l'option --env"""
    return request.config.getoption("--env")


def test_environment(env):
    """Utiliser l'option"""
    print(f"\nRunning in {env} environment")
    assert env in ["local", "staging", "prod"]


# Utilisation:
# pytest --env=staging


"""
PLUGIN PACKAGE (Distributable)
-----------------------------
"""

# pytest_myplugin.py
import pytest

def pytest_configure(config):
    """Hook de configuration"""
    config.addinivalue_line(
        "markers",
        "myplugin: custom marker from myplugin"
    )


@pytest.fixture
def my_fixture():
    """Fixture du plugin"""
    return "plugin data"


# setup.py pour distribuer
from setuptools import setup

setup(
    name="pytest-myplugin",
    version="0.1.0",
    py_modules=["pytest_myplugin"],
    entry_points={
        "pytest11": [
            "myplugin = pytest_myplugin",
        ],
    },
    install_requires=["pytest>=7.0"],
)


"""
[DOCS] RÉCAPITULATIF PLUGINS
-----------------------

Vous avez appris:
[OK] pytest-cov: Coverage de code
[OK] pytest-xdist: Parallélisation
[OK] pytest-timeout: Timeouts
[OK] pytest-mock: Mocking simplifié
[OK] pytest-django: Tests Django
[OK] pytest-asyncio: Tests async
[OK] pytest-bdd: BDD
[OK] pytest-benchmark: Performance
[OK] pytest-html: Rapports HTML
[OK] pytest-sugar: Output joli

Points clés:
[CLE] 1000+ plugins disponibles
[CLE] Facile à installer (pip install)
[CLE] Configuration dans pytest.ini
[CLE] Créer ses propres plugins


[OBJECTIF] PLUGINS ESSENTIELS POUR PROJET PRO:

OBLIGATOIRES:
- pytest-cov (coverage)
- pytest-xdist (parallélisation)
- pytest-timeout (sécurité)

RECOMMANDÉS:
- pytest-mock (mocking)
- pytest-html (rapports)
- pytest-sugar (UX)

SELON PROJET:
- pytest-django (Django)
- pytest-asyncio (async)
- pytest-benchmark (performance)


PROCHAIN CHAPITRE:
Debugging et best practices finales!
"""


# ============================================================================
# [GUIDE] CHAPITRE 13: DEBUGGING ET BEST PRACTICES
# ============================================================================

"""
[OBJECTIF] OBJECTIF DU CHAPITRE
Maîtriser le debugging de tests et les best practices professionnelles.


DEBUGGING DE TESTS
-----------------

[IDEE] Plusieurs techniques pour debugger des tests qui échouent
"""

# 1. VERBOSE OUTPUT
pytest -v               # Verbose
pytest -vv              # Très verbose
pytest -vvv             # Maximum verbose


# 2. AFFICHER LES PRINT
pytest -s               # Pas de capture de stdout
pytest --capture=no     # Même chose


# 3. TRACEBACK DÉTAILLÉ
pytest --tb=long        # Traceback long (défaut)
pytest --tb=short       # Traceback court
pytest --tb=line        # Une ligne par erreur
pytest --tb=native      # Style Python natif
pytest --tb=no          # Pas de traceback


# 4. ARRÊTER AU PREMIER ÉCHEC
pytest -x               # Stop at first failure
pytest --maxfail=3      # Stop after 3 failures


# 5. DERNIERS TESTS ÉCHOUÉS
pytest --lf             # Last failed (relancer seulement ceux qui ont échoué)
pytest --ff             # Failed first (échoués d'abord, puis les autres)


# 6. PDB (Python Debugger)
pytest --pdb            # Entrer en PDB à chaque échec
pytest --pdbcls=IPython.terminal.debugger:TerminalPdb  # Utiliser IPython


"""
UTILISER PDB DANS LES TESTS
--------------------------
"""

def test_with_debugger():
    """
    [IDEE] DEBUGGER INTERACTIF
    """
    x = 10
    y = 20
    
    # Breakpoint manuel
    import pdb; pdb.set_trace()
    
    # Ou Python 3.7+
    breakpoint()
    
    result = x + y
    assert result == 30


"""
Commandes PDB utiles:
    l (list) : Voir le code
    n (next) : Ligne suivante
    s (step) : Entrer dans fonction
    c (continue) : Continuer
    p variable : Print variable
    pp variable : Pretty print
    h : Help
    q : Quit


PYTEST --TRACE
-------------
"""

def test_interactive_debug():
    """
    [IDEE] BREAKPOINT AUTOMATIQUE AU DÉBUT
    """
    # pytest --trace entrera en PDB ici
    result = complex_function()
    assert result == expected


# Lancer avec:
# pytest --trace test_file.py::test_interactive_debug


"""
LOGGING DANS LES TESTS
--------------------
"""

import logging

logger = logging.getLogger(__name__)

def test_with_logging(caplog):
    """
    [IDEE] FIXTURE caplog: Capturer logs
    """
    # Configurer niveau
    caplog.set_level(logging.INFO)
    
    # Code qui log
    logger.info("Starting test")
    my_function()
    logger.info("Test complete")
    
    # Vérifier logs
    assert "Starting test" in caplog.text
    assert len(caplog.records) == 2


"""
BEST PRACTICES
-------------

[IDEE] Règles d'or pour tests professionnels
"""


# ====================================================================
# BEST PRACTICE 1: NOMMAGE DESCRIPTIF
# ====================================================================

# [X] MAUVAIS
def test_user():
    pass

def test_1():
    pass


# [OK] BON
def test_user_creation_with_valid_data_succeeds():
    """Test que la création d'user avec données valides réussit"""
    pass

def test_user_creation_with_duplicate_email_fails():
    """Test que créer un user avec email dupliqué échoue"""
    pass


"""
[IDEE] PATTERN DE NOMMAGE:

test_<fonction>_<condition>_<résultat_attendu>

Exemples:
- test_login_with_valid_credentials_returns_token
- test_login_with_invalid_password_returns_401
- test_create_post_without_authentication_raises_error


# ====================================================================
# BEST PRACTICE 2: STRUCTURE AAA (Arrange-Act-Assert)
# ====================================================================
"""

def test_user_update():
    """
    [IDEE] STRUCTURE AAA: Toujours visible!
    """
    # ARRANGE: Préparer les données
    user = User(username="alice", email="alice@example.com")
    user.save()
    
    # ACT: Exécuter l'action
    user.username = "alice_updated"
    user.save()
    
    # ASSERT: Vérifier le résultat
    updated_user = User.objects.get(id=user.id)
    assert updated_user.username == "alice_updated"


"""
[IDEE] POURQUOI AAA?

[OK] Lisibilité
[OK] Structure claire
[OK] Facile à maintenir
[OK] Standard de l'industrie


# ====================================================================
# BEST PRACTICE 3: UN CONCEPT PAR TEST
# ====================================================================
"""

# [X] MAUVAIS: Plusieurs concepts dans un test
def test_user_everything():
    """Test TOUT sur les users (trop!)"""
    # Création
    user = User("alice")
    assert user.id is not None
    
    # Mise à jour
    user.username = "bob"
    assert user.username == "bob"
    
    # Suppression
    user.delete()
    assert User.get(user.id) is None


# [OK] BON: Un test par concept
def test_user_creation_generates_id():
    """Test seulement la création"""
    user = User("alice")
    assert user.id is not None


def test_user_update_changes_username():
    """Test seulement la mise à jour"""
    user = create_user("alice")
    user.username = "bob"
    assert user.username == "bob"


def test_user_deletion_removes_from_database():
    """Test seulement la suppression"""
    user = create_user("alice")
    user_id = user.id
    user.delete()
    assert User.get(user_id) is None


"""
[IDEE] UN TEST = UNE ASSERTION PRINCIPALE

(Assertions secondaires OK pour vérifier le setup)


# ====================================================================
# BEST PRACTICE 4: TESTS INDÉPENDANTS
# ====================================================================
"""

# [X] MAUVAIS: Tests interdépendants
test_user_id = None

def test_create_user():
    """Test 1: Dépendance!"""
    global test_user_id
    user = User("alice")
    test_user_id = user.id  # <- État partagé!


def test_update_user():
    """Test 2: Dépend du test 1!"""
    user = User.get(test_user_id)  # <- Peut échouer si test_1 échoue
    user.username = "bob"


# [OK] BON: Tests indépendants
@pytest.fixture
def user():
    """Fixture: Chaque test a son user"""
    user = User("alice")
    yield user
    user.delete()


def test_create_user_returns_user_object():
    """Test 1: Indépendant"""
    user = User("alice")
    assert isinstance(user, User)


def test_update_user_changes_username(user):
    """Test 2: Indépendant (utilise fixture)"""
    user.username = "bob"
    assert user.username == "bob"


"""
[IDEE] RÈGLE:
Tests doivent pouvoir s'exécuter:
- Dans n'importe quel ordre
- Individuellement
- En parallèle


# ====================================================================
# BEST PRACTICE 5: FIXTURES POUR SETUP COMPLEXE
# ====================================================================
"""

# [X] MAUVAIS: Setup dupliqué
def test_post_creation():
    db = Database()
    db.connect()
    user = User("alice")
    db.save(user)
    
    post = Post("Hello", author=user)
    assert post.title == "Hello"
    
    db.disconnect()


def test_post_update():
    db = Database()  # <- Dupliqué!
    db.connect()
    user = User("alice")
    db.save(user)
    
    post = Post("Hello", author=user)
    post.title = "Updated"
    assert post.title == "Updated"
    
    db.disconnect()


# [OK] BON: Fixtures
@pytest.fixture
def db():
    """Fixture DB"""
    db = Database()
    db.connect()
    yield db
    db.disconnect()


@pytest.fixture
def user(db):
    """Fixture user"""
    user = User("alice")
    db.save(user)
    return user


def test_post_creation(user):
    """Setup simplifié avec fixtures"""
    post = Post("Hello", author=user)
    assert post.title == "Hello"


def test_post_update(user):
    """Pas de duplication!"""
    post = Post("Hello", author=user)
    post.title = "Updated"
    assert post.title == "Updated"


"""
# ====================================================================
# BEST PRACTICE 6: TESTER LES EDGE CASES
# ====================================================================
"""

def divide(a, b):
    """Fonction à tester"""
    return a / b


# [X] MAUVAIS: Seulement le cas normal
def test_divide_normal_case():
    assert divide(10, 2) == 5


# [OK] BON: Tous les cas
@pytest.mark.parametrize("a,b,expected", [
    # Cas normaux
    (10, 2, 5),
    (9, 3, 3),
    
    # Nombres négatifs
    (-10, 2, -5),
    (10, -2, -5),
    (-10, -2, 5),
    
    # Zéro au numérateur
    (0, 5, 0),
    
    # Floats
    (5.5, 2, 2.75),
])
def test_divide_normal_cases(a, b, expected):
    assert divide(a, b) == expected


def test_divide_by_zero_raises_error():
    """Edge case: division par zéro"""
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)


"""
[IDEE] EDGE CASES COURANTS:

- Valeurs nulles (None, 0, "", [], {})
- Valeurs négatives
- Très grandes valeurs
- Valeurs limites (min, max)
- Cas d'erreur


# ====================================================================
# BEST PRACTICE 7: MESSAGES D'ERREUR CLAIRS
# ====================================================================
"""

# [X] MAUVAIS: Pas de contexte
def test_user_age():
    user = User("alice", age=25)
    assert user.age > 18


# [OK] BON: Message clair
def test_user_age_is_adult():
    user = User("alice", age=25)
    assert user.age > 18, \
        f"User {user.username} with age {user.age} should be adult (>18)"


"""
Quand le test échoue, vous voyez:
    AssertionError: User alice with age 15 should be adult (>18)

Beaucoup plus utile que juste:
    AssertionError: assert 15 > 18


# ====================================================================
# BEST PRACTICE 8: ORGANISATION DES FICHIERS
# ====================================================================
"""

# Structure recommandée
project/
├── src/
│   └── myapp/
│       ├── __init__.py
│       ├── models.py
│       ├── services.py
│       └── utils.py
├── tests/
│   ├── conftest.py           # Fixtures globales
│   ├── unit/
│   │   ├── conftest.py       # Fixtures unit
│   │   ├── test_models.py
│   │   ├── test_services.py
│   │   └── test_utils.py
│   ├── integration/
│   │   ├── conftest.py       # Fixtures integration
│   │   ├── test_api.py
│   │   └── test_database.py
│   └── fixtures/             # Données de test
│       ├── users.json
│       └── posts.json
├── pytest.ini
└── requirements-dev.txt      # pytest, pytest-cov, etc.


"""
[IDEE] PRINCIPES:

1. Miroir de la structure src/
   src/myapp/models.py -> tests/unit/test_models