using MaBibli.Api.Data;
using MaBibli.Shared.Entites;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
namespace MaBibli.Tests;
///
/// Rattrapage des colonnes normalisées après une migration.
///
///
/// Les données sont écrites telles que la migration SQL les laisse : normalisation
/// approchée (lower() ne touche pas aux accents), donc variantes d'un même auteur encore
/// éclatées. C'est exactement l'état que ce service doit savoir rattraper.
///
public class ServiceRenormalisationTests : IDisposable
{
private readonly SqliteConnection _connexion;
private readonly MaBibliDbContext _db;
public ServiceRenormalisationTests()
{
_connexion = new SqliteConnection("Data Source=:memory:");
_connexion.Open();
_db = new MaBibliDbContext(
new DbContextOptionsBuilder().UseSqlite(_connexion).Options);
_db.Database.EnsureCreated();
}
private Task ExecuterAsync() =>
new ServiceRenormalisation(_db, NullLogger.Instance).ExecuterAsync();
/// Écrit un auteur comme la migration le ferait : formes en lower() brut.
private Auteur AjouterHerite(string nom, params string[] titres)
{
var auteur = new Auteur
{
Nom = nom,
NomNormalise = nom.ToLowerInvariant(),
CleRegroupement = nom.ToLowerInvariant(),
};
_db.Auteurs.Add(auteur);
foreach (var titre in titres)
{
var livre = new Livre { Titre = titre, TitreNormalise = titre.ToLowerInvariant() };
_db.Livres.Add(livre);
livre.Auteurs.Add(new LivreAuteur { Auteur = auteur });
}
_db.SaveChanges();
return auteur;
}
[Fact]
public async Task Les_titres_perdent_leurs_accents_dans_la_colonne_normalisee()
{
AjouterHerite("Émile Zola", "La Bête humaine");
await ExecuterAsync();
Assert.Equal("la bete humaine", _db.Livres.Single().TitreNormalise);
}
[Fact]
public async Task Les_variantes_daccent_et_dordre_sont_reunies()
{
AjouterHerite("Emile Zola", "Germinal");
AjouterHerite("Émile Zola", "Nana");
AjouterHerite("ZOLA, Émile", "La Curée");
await ExecuterAsync();
var zola = Assert.Single(_db.Auteurs);
Assert.Equal(3, await _db.LivreAuteurs.CountAsync(la => la.AuteurId == zola.Id));
}
///
/// Le nom qui survit est celui qui se lit le mieux. Sans arbitrage, c'est l'ordre — arbitraire —
/// des identifiants issus de la migration qui déciderait, et la bibliothèque perdrait ses
/// accents au profit de la saisie la plus négligée.
///
[Theory]
// L'ordre est celui des identifiants après migration : le résultat doit en être indépendant.
[InlineData("Emile Zola|Émile Zola")]
[InlineData("Émile Zola|Emile Zola")]
[InlineData("EMILE ZOLA|ZOLA, Émile|Émile Zola")]
[InlineData("ZOLA, Émile|Émile Zola")]
public async Task Le_nom_conserve_est_le_plus_presentable(string variantes)
{
foreach (var variante in variantes.Split('|'))
{
AjouterHerite(variante, $"Livre de {variante}");
}
await ExecuterAsync();
Assert.Equal("Émile Zola", Assert.Single(_db.Auteurs).Nom);
}
///
/// Les initiales échappent à la clé de regroupement. Sans second tour, une base héritée
/// resterait éclatée là où une saisie neuve aurait été réunie d'emblée.
///
[Fact]
public async Task Les_initiales_abregees_sont_reunies_sous_le_nom_le_plus_complet()
{
AjouterHerite("P.F. Hamilton", "Pandora's Star");
AjouterHerite("Peter F. Hamilton", "Judas Unchained");
await ExecuterAsync();
var hamilton = Assert.Single(_db.Auteurs);
Assert.Equal("Peter F. Hamilton", hamilton.Nom);
Assert.Equal(2, await _db.LivreAuteurs.CountAsync());
}
/// Le cas ambigu reste ambigu : il sera proposé, pas appliqué.
[Fact]
public async Task Un_patronyme_seul_nest_jamais_absorbe()
{
AjouterHerite("Hamilton", "Great North Road");
AjouterHerite("Peter F. Hamilton", "Pandora's Star");
await ExecuterAsync();
Assert.Equal(2, await _db.Auteurs.CountAsync());
}
[Fact]
public async Task Un_livre_cosigne_par_deux_variantes_ne_garde_quun_lien()
{
var livre = new Livre { Titre = "Œuvre commune", TitreNormalise = "œuvre commune" };
_db.Livres.Add(livre);
_db.SaveChanges();
foreach (var nom in new[] { "Emile Zola", "Émile Zola" })
{
var auteur = new Auteur
{
Nom = nom,
NomNormalise = nom.ToLowerInvariant(),
CleRegroupement = nom.ToLowerInvariant(),
};
_db.Auteurs.Add(auteur);
_db.LivreAuteurs.Add(new LivreAuteur { LivreId = livre.Id, Auteur = auteur });
}
_db.SaveChanges();
await ExecuterAsync();
Assert.Single(_db.Auteurs);
Assert.Equal(1, await _db.LivreAuteurs.CountAsync());
}
[Fact]
public async Task Un_second_passage_ne_change_plus_rien()
{
AjouterHerite("Emile Zola", "Germinal");
AjouterHerite("Émile Zola", "Nana");
await ExecuterAsync();
var apresPremier = _db.Auteurs.Single();
var nom = apresPremier.Nom;
var cle = apresPremier.CleRegroupement;
await ExecuterAsync();
var apresSecond = Assert.Single(_db.Auteurs);
Assert.Equal(nom, apresSecond.Nom);
Assert.Equal(cle, apresSecond.CleRegroupement);
Assert.Equal(2, await _db.LivreAuteurs.CountAsync());
}
[Fact]
public async Task Une_base_vide_ne_pose_aucun_probleme()
{
await ExecuterAsync();
Assert.Empty(_db.Auteurs);
Assert.Empty(_db.Livres);
}
// ── Rattrapage des tables plus récentes ─────────────────────────────────
//
// Envies, séries, revues et numéros calculent leurs formes À L'ÉCRITURE : sans ce passage,
// un changement de règle de normalisation — comme le développement des ligatures le
// 2026-08-19 — laisserait ces lignes sur leur ancienne clé, donc introuvables par la
// nouvelle. Trois de ces colonnes portent en plus une UNICITÉ, et une collision se paierait
// au démarrage du serveur.
[Fact]
public async Task Une_envie_ecrite_avant_le_changement_de_regle_est_rattrapee()
{
// Forme telle qu'elle aurait été calculée avant le développement des ligatures.
_db.LivresSouhaites.Add(new LivreSouhaite
{
Utilisateur = "mathieu",
Titre = "L'Œuvre",
TitreNormalise = "l œuvre",
Auteur = "Émile Zola",
AuteurNormalise = "emile zola",
});
await _db.SaveChangesAsync();
await ExecuterAsync();
Assert.Equal("l oeuvre", _db.LivresSouhaites.Single().TitreNormalise);
}
[Fact]
public async Task Une_serie_et_une_revue_sont_rattrapees_aussi()
{
_db.Series.Add(new Serie { Titre = "Les Sœurs", TitreNormalise = "les sœurs" });
_db.Revues.Add(new Revue { Titre = "Cœur", TitreNormalise = "cœur" });
await _db.SaveChangesAsync();
await ExecuterAsync();
Assert.Equal("les soeurs", _db.Series.Single().TitreNormalise);
Assert.Equal("coeur", _db.Revues.Single().TitreNormalise);
}
///
/// ⚠️ Le cas qui empêcherait le serveur de démarrer.
///
///
/// La nouvelle règle rend identiques deux séries qui ne l'étaient pas. L'index unique
/// refuserait l'écriture, et l'exception surviendrait au démarrage : l'application ne
/// se lancerait plus du tout. La plus ancienne prend la nouvelle forme, la suivante garde
/// l'ancienne — mal normalisée vaut infiniment mieux que supprimée, ou qu'un serveur mort.
///
[Fact]
public async Task Une_collision_dunicite_ne_fait_pas_echouer_le_demarrage()
{
_db.Series.Add(new Serie { Titre = "L'Œuvre", TitreNormalise = "l œuvre" });
_db.Series.Add(new Serie { Titre = "L'oeuvre", TitreNormalise = "l oeuvre" });
await _db.SaveChangesAsync();
await ExecuterAsync();
var series = _db.Series.OrderBy(s => s.Id).ToList();
Assert.Equal(2, series.Count);
Assert.Equal(["l oeuvre", "l œuvre"], series.Select(s => s.TitreNormalise).Order());
}
///
/// Une ligne déjà correcte ne doit pas se bloquer elle-même.
///
///
/// Le jeu des clés occupées part de l'état actuel de la base : sans la libération de sa
/// propre clé, toute ligne dont la forme change verrait sa nouvelle valeur « déjà prise »
/// dès qu'elle est égale à son ancienne — et rien ne serait jamais corrigé.
///
[Fact]
public async Task Le_rattrapage_reste_idempotent()
{
_db.Revues.Add(new Revue { Titre = "Cœur", TitreNormalise = "cœur" });
await _db.SaveChangesAsync();
await ExecuterAsync();
await ExecuterAsync();
Assert.Equal("coeur", _db.Revues.Single().TitreNormalise);
}
public void Dispose()
{
_db.Dispose();
_connexion.Dispose();
GC.SuppressFinalize(this);
}
}