# language: fr

Fonctionnalité: Recherche de contenu
  En tant qu'utilisateur de RoadWave
  Je veux rechercher des contenus audio par mots-clés, localisation et filtres
  Afin de trouver facilement le contenu qui m'intéresse

  Contexte:
    Étant donné que l'application RoadWave est démarrée
    Et que l'utilisateur "jean@example.com" est connecté

  # 15.3.1 - Recherche par mot-clé

  Scénario: Recherche full-text basique
    Étant donné que la base contient les contenus suivants:
      | titre                  | description                    | créateur        |
      | Balade à Paris         | Visite du quartier Latin       | @paris_stories  |
      | Secrets de Montmartre  | Histoire de la butte           | @explore_paris  |
      | Voyage en Normandie    | Découverte des plages          | @voyages_fr     |
    Quand l'utilisateur recherche "paris"
    Alors 2 résultats sont retournés
    Et les résultats incluent "Balade à Paris"
    Et les résultats incluent "Secrets de Montmartre"

  Scénario: Recherche avec stemming français
    Étant donné un contenu avec le titre "Voyage en Bretagne"
    Quand l'utilisateur recherche "voyages"
    Alors le contenu "Voyage en Bretagne" est trouvé
    Et le stemming a transformé "voyages" en racine "voyag"

  Plan du Scénario: Stemming français sur différentes formes
    Étant donné un contenu avec le mot "<mot_original>"
    Quand l'utilisateur recherche "<recherche>"
    Alors le contenu est trouvé grâce au stemming français

    Exemples:
      | mot_original | recherche  |
      | voyage       | voyages    |
      | voyager      | voyage     |
      | balades      | balade     |
      | historique   | histoire   |

  Scénario: Recherche avec accents ignorés
    Étant donné un contenu avec le titre "Découverte de l'Élysée"
    Quand l'utilisateur recherche "decouverte elysee"
    Alors le contenu est trouvé
    Et les accents sont normalisés automatiquement

  Scénario: Champs indexés avec pondération
    Étant donné les contenus suivants:
      | titre          | description     | créateur       | tags           |
      | Voyage Paris   | Balade sympa    | @user1         | Tourisme       |
      | Balade Lyon    | Voyage en ville | @paris_guide   | Voyage         |
    Quand l'utilisateur recherche "paris"
    Alors "Voyage Paris" est en première position
    Parce que le titre a un poids × 3
    Et "@paris_guide" apparaît en second
    Parce que le créateur a un poids × 2

  Scénario: Ranking par pertinence et popularité
    Étant donné les contenus suivants:
      | titre          | écoutes | rang_texte |
      | Balade Paris   | 50000   | 0.8        |
      | Paris la nuit  | 1000    | 0.9        |
    Quand l'utilisateur recherche "paris"
    Alors le score final combine rang_texte × (1 + log(écoutes + 1))
    Et "Balade Paris" est mieux classé grâce à sa popularité

  Scénario: Autocomplete pendant la frappe
    Étant donné que l'utilisateur commence à taper "par"
    Quand 3 caractères sont saisis
    Alors des suggestions apparaissent:
      | suggestion           |
      | paris                |
      | parc naturel         |
      | parvis notre-dame    |
    Et le top 5 des suggestions est affiché

  Scénario: Historique des 10 dernières recherches
    Étant donné que l'utilisateur a effectué les recherches suivantes:
      | recherche           | date       |
      | voyage paris        | 2026-01-20 |
      | audio-guide louvre  | 2026-01-19 |
      | podcast automobile  | 2026-01-18 |
    Quand l'utilisateur ouvre la barre de recherche
    Alors les 10 dernières recherches sont affichées
    Et elles sont triées par date décroissante

  Scénario: Correction automatique si aucun résultat
    Étant donné que l'utilisateur recherche "ballade paris" (faute d'orthographe)
    Et qu'aucun résultat n'est trouvé
    Quand la page de résultats s'affiche
    Alors une suggestion "Essayez plutôt : balade paris" est affichée

  Scénario: Recherches populaires suggérées
    Étant donné qu'aucun résultat n'est trouvé pour une recherche
    Quand la page s'affiche
    Alors des suggestions populaires sont affichées:
      | suggestion               |
      | balade paris             |
      | audio-guide louvre       |
      | visite montmartre        |

  # 15.3.2 - Recherche géographique

  Scénario: Saisie d'un lieu avec autocomplete
    Étant donné que l'utilisateur ouvre le filtre "Lieu"
    Quand il tape "Louv"
    Alors Nominatim retourne des suggestions:
      | suggestion              | type       |
      | Musée du Louvre, Paris  | monument   |
      | Louvres, Val-d'Oise     | commune    |

  Scénario: Sélection d'un lieu et définition du rayon
    Étant donné que l'utilisateur sélectionne "Paris, France"
    Et que les coordonnées sont (48.8566, 2.3522)
    Quand il définit un rayon de 50 km
    Alors la recherche PostGIS utilise ST_DWithin avec 50000 mètres

  Plan du Scénario: Recherche géographique avec différents rayons
    Étant donné un contenu à 30 km de Paris
    Quand l'utilisateur recherche autour de Paris avec un rayon de <rayon>
    Alors le contenu est <résultat>

    Exemples:
      | rayon  | résultat      |
      | 20 km  | non trouvé    |
      | 50 km  | trouvé        |
      | 100 km | trouvé        |

  Scénario: Utilisation de "Autour de moi" (GPS actuel)
    Étant donné que l'utilisateur active le GPS
    Et que sa position est (48.8566, 2.3522)
    Quand il sélectionne "Autour de moi"
    Alors la recherche utilise ses coordonnées GPS actuelles
    Et un rayon par défaut de 10 km est appliqué

  Scénario: Curseur de rayon avec limites
    Étant donné que l'utilisateur ouvre le curseur de rayon
    Quand il ajuste le curseur
    Alors les valeurs disponibles vont de 5 km à 500 km
    Et la valeur s'affiche en temps réel "50 km"

  Scénario: Affichage de la distance dans les résultats
    Étant donné une recherche géographique autour de Paris
    Et un contenu à 2.3 km de distance
    Quand les résultats sont affichés
    Alors la distance "À 2.3 km" est indiquée pour chaque résultat

  Plan du Scénario: Tri par proximité géographique
    Étant donné des contenus à différentes distances de Paris:
      | contenu        | distance |
      | Louvre Guide   | 0.5 km   |
      | Tour Eiffel    | 2.0 km   |
      | Versailles     | 20 km    |
    Quand l'utilisateur trie par "Proximité"
    Alors les résultats sont affichés dans l'ordre:
      | position | contenu        |
      | 1        | Louvre Guide   |
      | 2        | Tour Eiffel    |
      | 3        | Versailles     |

  Scénario: Géocodage avec Nominatim (MVP)
    Étant donné que l'application est en phase MVP
    Quand une requête de géocodage est effectuée
    Alors l'API publique Nominatim est utilisée
    Et le rate limit de 1 req/s est respecté

  Scénario: Géocodage avec fallback Mapbox
    Étant donné que Nominatim ne retourne aucun résultat
    Quand l'application tente un fallback
    Alors l'API Mapbox Geocoding est utilisée
    Et le coût de 0.50€ / 1000 requêtes est appliqué

  # 15.3.3 - Filtres avancés

  Scénario: Ouverture du panneau de filtres
    Étant donné que l'utilisateur est sur la page de recherche
    Quand il clique sur "Filtres"
    Alors un panneau latéral s'ouvre
    Et 7 catégories de filtres sont affichées:
      | catégorie            |
      | Type de contenu      |
      | Durée                |
      | Classification âge   |
      | Géo-pertinence       |
      | Tags                 |
      | Date de publication  |
      | Abonnement           |

  Scénario: Filtre par type de contenu (multi-sélection)
    Étant donné que l'utilisateur ouvre les filtres
    Quand il sélectionne:
      | type              |
      | Contenu court     |
      | Audio-guide       |
    Alors seuls ces types de contenus sont recherchés
    Et les podcasts et radios live sont exclus

  Plan du Scénario: Filtre par durée
    Étant donné un contenu de <durée> minutes
    Quand l'utilisateur filtre par "<tranche>"
    Alors le contenu est <résultat>

    Exemples:
      | durée | tranche      | résultat   |
      | 3     | <5 min       | trouvé     |
      | 3     | 5-15 min     | non trouvé |
      | 10    | 5-15 min     | trouvé     |
      | 20    | 15-30 min    | trouvé     |
      | 45    | >30 min      | trouvé     |

  Scénario: Filtre par classification âge
    Étant donné des contenus avec différentes classifications:
      | contenu         | classification |
      | Conte enfants   | Tout public    |
      | Podcast news    | 13+            |
      | Débat politique | 16+            |
    Quand l'utilisateur filtre "Tout public"
    Alors seul "Conte enfants" est affiché

  Scénario: Filtre par géo-pertinence
    Étant donné des contenus avec différents types géo:
      | contenu           | type_geo   |
      | Guide Louvre      | Ancré      |
      | Podcast Paris     | Contextuel |
      | News nationales   | Neutre     |
    Quand l'utilisateur filtre "Ancré, Contextuel"
    Alors "Guide Louvre" et "Podcast Paris" sont affichés
    Et "News nationales" est exclu

  Scénario: Filtre par tags (multi-sélection)
    Étant donné des contenus taggés:
      | contenu              | tags                    |
      | Voyage en Italie     | Voyage, Gastronomie     |
      | Histoire de Rome     | Voyage, Histoire        |
      | Économie italienne   | Économie                |
    Quand l'utilisateur sélectionne les tags "Voyage, Histoire"
    Alors "Histoire de Rome" est en priorité (2 tags correspondants)
    Et "Voyage en Italie" est affiché (1 tag correspondant)
    Et "Économie italienne" est exclu

  Plan du Scénario: Filtre par date de publication
    Étant donné un contenu publié il y a <délai>
    Quand l'utilisateur filtre par "<période>"
    Alors le contenu est <résultat>

    Exemples:
      | délai      | période           | résultat   |
      | 12 heures  | Dernières 24h     | trouvé     |
      | 3 jours    | Cette semaine     | trouvé     |
      | 15 jours   | Ce mois           | trouvé     |
      | 8 mois     | Cette année       | trouvé     |
      | 2 ans      | Toutes dates      | trouvé     |
      | 2 ans      | Cette année       | non trouvé |

  Scénario: Filtre par type d'abonnement
    Étant donné des contenus gratuits et Premium:
      | contenu            | type     |
      | Balade Paris       | Gratuit  |
      | Visite VIP Louvre  | Premium  |
    Quand l'utilisateur filtre "Premium uniquement 👑"
    Alors seul "Visite VIP Louvre" est affiché

  Scénario: Combinaison de filtres multiples (AND logic)
    Étant donné que l'utilisateur applique les filtres:
      | filtre              | valeur           |
      | Type                | Audio-guide      |
      | Durée               | 5-15 min         |
      | Tags                | Voyage           |
      | Classification      | Tout public      |
    Quand la recherche est lancée
    Alors seuls les contenus respectant TOUS les critères sont affichés

  Scénario: Réinitialisation des filtres
    Étant donné que l'utilisateur a appliqué 5 filtres différents
    Quand il clique sur "Réinitialiser"
    Alors tous les filtres sont désactivés
    Et la recherche affiche tous les résultats

  Scénario: Sauvegarde d'une recherche
    Étant donné que l'utilisateur a appliqué plusieurs filtres
    Quand il clique sur "💾 Sauvegarder cette recherche"
    Et qu'il entre le nom "Podcasts voyage Paris"
    Alors la recherche est sauvegardée
    Et elle apparaît dans l'onglet "Recherches sauvegardées"

  Scénario: Limite de 5 recherches sauvegardées
    Étant donné que l'utilisateur a déjà 5 recherches sauvegardées
    Quand il tente de sauvegarder une 6ème recherche
    Alors un message d'erreur s'affiche
    Et il doit supprimer une recherche existante avant d'en ajouter une nouvelle

  Scénario: Notifications pour recherches sauvegardées
    Étant donné une recherche sauvegardée "Podcasts voyage Paris"
    Et que l'utilisateur a activé les notifications
    Quand 3 nouveaux contenus correspondants sont publiés
    Alors une notification "3 nouveaux contenus dans 'Podcasts voyage Paris'" est envoyée

  Plan du Scénario: Options de tri des résultats
    Étant donné une recherche avec plusieurs résultats
    Quand l'utilisateur sélectionne le tri "<option>"
    Alors les résultats sont triés selon <algorithme>

    Exemples:
      | option      | algorithme                                    |
      | Pertinence  | Score recherche × (1 + log(écoutes + 1))     |
      | Popularité  | Écoutes complètes derniers 30j DESC           |
      | Récent      | Date publication DESC                         |
      | Proximité   | Distance GPS ASC (si recherche géo)           |
      | Durée       | Durée audio ASC ou DESC                       |

  # 15.3.4 - Page de résultats

  Scénario: Structure d'un résultat de recherche
    Étant donné un résultat de recherche
    Quand la page est affichée
    Alors chaque résultat contient:
      | élément            | exemple                  |
      | Cover image        | 120×68 px (16:9)         |
      | Titre              | Balade à Paris (2 lignes max) |
      | Créateur           | @paris_stories ✓         |
      | Durée              | 12 min                   |
      | Écoutes            | 🎧 2.3K                 |
      | Localisation       | 📍 Paris 5e · Ancré     |
      | Tags               | 🏷️ #Voyage #Histoire   |
      | Badge Premium      | 👑 (si applicable)      |
      | Distance           | À 2.3 km (si recherche géo) |
      | Bouton lecture     | ▶️ Écouter              |
      | Menu contextuel    | ⋮                        |

  Scénario: Lazy loading des images
    Étant donné une page avec 20 résultats de recherche
    Quand la page se charge
    Alors seules les 5 premières images sont chargées
    Et les images suivantes se chargent au scroll

  Scénario: Troncature du titre sur 2 lignes maximum
    Étant donné un contenu avec un titre de 120 caractères
    Quand le résultat est affiché
    Alors le titre est tronqué après 2 lignes
    Et "..." est ajouté à la fin

  Scénario: Lien cliquable vers le profil créateur
    Étant donné un résultat de recherche pour "@paris_stories"
    Quand l'utilisateur clique sur "@paris_stories"
    Alors il est redirigé vers "https://roadwave.fr/@paris_stories"

  Scénario: Menu contextuel d'un résultat [⋮]
    Étant donné que l'utilisateur clique sur [⋮] pour un résultat
    Quand le menu s'ouvre
    Alors les actions suivantes sont disponibles:
      | action                  |
      | Partager                |
      | Ajouter à une playlist  |
      | Télécharger (offline)   |
      | Signaler                |

  Scénario: Pagination avec 20 résultats par page
    Étant donné une recherche retournant 100 résultats
    Quand la page est affichée
    Alors 20 résultats sont chargés initialement
    Et un indicateur "1-20 sur 100 résultats" est visible

  Scénario: Infinite scroll automatique
    Étant donné que l'utilisateur scroll dans les résultats
    Quand il atteint 80% de la page
    Alors les 20 résultats suivants sont chargés automatiquement
    Et un loader est affiché pendant le chargement

  Scénario: Bouton fallback "Charger 20 suivants"
    Étant donné que l'infinite scroll est désactivé (paramètres)
    Quand l'utilisateur atteint la fin de la page
    Alors un bouton "Charger 20 suivants" est affiché
    Et les résultats se chargent au clic

  # Vue carte

  Scénario: Basculement entre vue liste et vue carte
    Étant donné que l'utilisateur est sur la page de résultats
    Quand il clique sur le toggle "Liste / Carte"
    Alors la vue carte Leaflet s'affiche
    Et les résultats sont affichés comme markers sur la carte

  Scénario: Affichage de la carte Leaflet
    Étant donné que la vue carte est activée
    Quand la carte se charge
    Alors la carte utilise les tuiles OpenStreetMap
    Et le centre est la position de recherche (ou GPS utilisateur)
    Et le zoom initial montre tous les résultats

  Scénario: Markers cliquables sur la carte
    Étant donné que 10 résultats sont affichés sur la carte
    Quand l'utilisateur clique sur un marker
    Alors une popup s'affiche avec:
      | élément          |
      | Titre            |
      | Créateur         |
      | Durée            |
      | Distance         |
      | Bouton ▶️ Écouter|

  Scénario: Clustering des markers proches
    Étant donné que 50 résultats sont très proches géographiquement
    Quand la carte est affichée
    Alors les markers proches sont groupés en clusters
    Et le nombre de contenus est affiché sur le cluster
    Et le cluster se décompose au zoom

  Scénario: Synchronisation liste / carte
    Étant donné que l'utilisateur est en vue carte
    Quand il clique sur un marker et écoute le contenu
    Et qu'il rebascule en vue liste
    Alors le contenu écouté est marqué dans la liste
    Et la position de scroll est maintenue

  # Performances et index

  Scénario: Index PostgreSQL full-text pour performances
    Étant donné que la base contient 100K contenus
    Quand une recherche full-text est effectuée
    Alors l'index GIN sur to_tsvector est utilisé
    Et la requête retourne en moins de 100ms

  Scénario: Index PostGIS GIST pour recherche géo
    Étant donné une recherche géographique avec rayon 50 km
    Quand la requête PostGIS ST_DWithin est exécutée
    Alors l'index GIST sur la colonne location est utilisé
    Et la requête retourne en moins de 50ms

  Scénario: Index composites pour filtres
    Étant donné une recherche avec filtres multiples
    Quand les filtres type, durée, âge, géo, date sont appliqués
    Alors l'index composite idx_content_filters est utilisé
    Et les performances restent optimales

  Scénario: Index GIN pour recherche par tags
    Étant donné une recherche filtrée par tags "Voyage, Histoire"
    Quand la requête est exécutée
    Alors l'index GIN sur la colonne tags est utilisé
    Et la recherche est performante même avec 500K contenus

  # Cas d'erreur

  Scénario: Aucun résultat trouvé
    Étant donné que l'utilisateur recherche "xyzabc123"
    Quand aucun résultat n'est trouvé
    Alors un message "Aucun résultat pour 'xyzabc123'" s'affiche
    Et des suggestions de recherches populaires sont proposées

  Scénario: Recherche vide
    Étant donné que l'utilisateur clique sur "Rechercher" sans saisir de texte
    Quand la recherche est lancée
    Alors un message "Veuillez entrer au moins 2 caractères" s'affiche

  Scénario: Erreur de géocodage Nominatim
    Étant donné que l'API Nominatim est indisponible
    Quand l'utilisateur tente une recherche géographique
    Alors un message "Service de localisation temporairement indisponible" s'affiche
    Et la recherche continue sans filtre géographique

  Scénario: GPS désactivé pour "Autour de moi"
    Étant donné que l'utilisateur a désactivé le GPS
    Quand il sélectionne "Autour de moi"
    Alors un message "Veuillez activer la localisation" s'affiche
    Et un bouton "Activer" ouvre les paramètres système

  Scénario: Timeout de recherche après 10 secondes
    Étant donné qu'une recherche complexe est lancée
    Quand la requête dépasse 10 secondes
    Alors la recherche est annulée
    Et un message "La recherche a pris trop de temps, veuillez réessayer" s'affiche
