using MaBibli.Client.Services; using MaBibli.Shared.Dtos; namespace MaBibli.Tests; /// /// Regroupement des tomes d'une série au catalogue (2026-08-22). /// /// /// L'écran n'est pas éprouvable ici, mais la règle l'est : ces tests portent sur ce qui a été /// décidé — la place du bloc, l'ordre des tomes à l'intérieur, et le fait que rien ne /// disparaisse ni ne se duplique. /// public class GroupementCatalogueTests { private static LivreDto Livre(int id, string titre) => new() { Id = id, Titre = titre, Format = MaBibli.Shared.Entites.Format.Physique, DateAjout = DateTime.UtcNow, }; private static SerieDto Serie(int id, string titre, params (int Position, int LivreId)[] tomes) => SousSerie(id, titre, null, 0, tomes); private static SerieDto SousSerie( int id, string titre, int? parenteId, int position, params (int Position, int LivreId)[] tomes) => new() { Id = id, Titre = titre, SerieParenteId = parenteId, Position = position, Elements = [ .. tomes.Select(t => new ElementSerieDto { Id = t.LivreId, Position = t.Position, Titre = $"tome {t.Position}", LivreId = t.LivreId, }), ], }; /// /// ⚠️ Le cas qui a tranché la décision : le bloc se pose à la place du PREMIER tome, pour /// que l'ordre général du catalogue ne bouge pas. /// [Fact] public void Le_bloc_prend_la_place_de_son_premier_tome() { List catalogue = [ Livre(1, "Dracula"), Livre(2, "L'Éclat de cristal"), Livre(3, "Dune"), Livre(4, "Terre natale"), ]; var entrees = GroupementCatalogue.Grouper( catalogue, [Serie(9, "La Légende de Drizzt", (1, 2), (0, 4))]); Assert.Collection( entrees, e => Assert.Equal("Dracula", Assert.Single(e.Livres).Titre), e => { Assert.Equal("La Légende de Drizzt", e.Serie!.Titre); // À l'intérieur, l'ordre est celui de LECTURE, pas celui du catalogue. Assert.Equal(["Terre natale", "L'Éclat de cristal"], e.Livres.Select(l => l.Titre)); }, e => Assert.Equal("Dune", Assert.Single(e.Livres).Titre)); } /// Aucun livre ne disparaît, aucun n'apparaît deux fois : c'est la règle du catalogue. [Fact] public void Le_regroupement_ne_perd_ni_ne_duplique_aucun_livre() { List catalogue = [Livre(1, "A"), Livre(2, "B"), Livre(3, "C")]; // Le livre 2 est rattaché à DEUX séries — le modèle l'autorise. var entrees = GroupementCatalogue.Grouper( catalogue, [Serie(1, "Zeta", (0, 2)), Serie(2, "Alpha", (0, 2), (1, 3))]); Assert.Equal([1, 2, 3], entrees.SelectMany(e => e.Livres).Select(l => l.Id).Order()); } /// /// ⚠️ La série retenue est la première par ordre alphabétique, et non celle que l'API a /// rendue en premier : sans cela, le bloc changerait de nom d'un chargement à l'autre. /// [Fact] public void Un_livre_de_deux_series_est_range_sous_la_premiere_alphabetiquement() { var entrees = GroupementCatalogue.Grouper( [Livre(2, "B")], [Serie(1, "Zeta", (0, 2)), Serie(2, "Alpha", (0, 2))]); Assert.Equal("Alpha", Assert.Single(entrees).Serie!.Titre); } /// Sans série connue — hors-ligne sans instantané, ou groupement défait — la liste est plate. [Fact] public void Sans_series_la_liste_reste_celle_du_catalogue() { List catalogue = [Livre(1, "A"), Livre(2, "B")]; var entrees = GroupementCatalogue.Grouper(catalogue, null); Assert.All(entrees, e => Assert.Null(e.Serie)); Assert.Equal(["A", "B"], entrees.Select(e => e.Livres[0].Titre)); } /// /// Un seul tome possédé forme quand même un bloc : c'est ce qui dit à quoi ce livre /// appartient, et ce qui mène à la série où figurent les tomes manquants. /// [Fact] public void Un_seul_tome_possede_forme_quand_meme_un_bloc() { var entrees = GroupementCatalogue.Grouper( [Livre(4, "Terre natale")], [Serie(9, "L'Elfe noir", (0, 4), (1, 5))]); Assert.Equal("L'Elfe noir", Assert.Single(entrees).Serie!.Titre); } /// /// Les tomes filtrés restent absents : le regroupement rassemble ce que la liste contient, /// il ne va pas rechercher ce qu'un filtre a écarté. /// [Fact] public void Un_filtre_ne_ramene_pas_les_tomes_ecartes() { var entrees = GroupementCatalogue.Grouper( [Livre(5, "L'Éclat de cristal")], [Serie(9, "Drizzt", (0, 4), (1, 5))]); Assert.Equal(["L'Éclat de cristal"], Assert.Single(entrees).Livres.Select(l => l.Titre)); } /// /// ⚠️ Le cas du 2026-08-22 : un cycle est une série de séries, et le catalogue doit le /// montrer — sans quoi « replier chaque niveau » n'aurait qu'un seul niveau à replier. /// [Fact] public void Un_cycle_coiffe_les_series_qu_il_contient() { List catalogue = [Livre(1, "Terre natale"), Livre(2, "L'Éclat de cristal")]; List series = [ Serie(1, "La Légende de Drizzt"), SousSerie(3, "La Trilogie des Elfes noirs", 1, 1, (0, 2)), SousSerie(2, "L'Elfe noir", 1, 0, (0, 1)), ]; var cycle = Assert.Single(GroupementCatalogue.Grouper(catalogue, series)); Assert.Equal("La Légende de Drizzt", cycle.Serie!.Titre); Assert.Empty(cycle.Livres); Assert.Equal(2, cycle.NombreLivres); // Les sous-séries suivent leur ordre de LECTURE dans le cycle, pas l'alphabet. Assert.Collection( cycle.SousEntrees, e => { Assert.Equal("L'Elfe noir", e.Serie!.Titre); Assert.Equal(["Terre natale"], e.Livres.Select(l => l.Titre)); }, e => { Assert.Equal("La Trilogie des Elfes noirs", e.Serie!.Titre); Assert.Equal(["L'Éclat de cristal"], e.Livres.Select(l => l.Titre)); }); } /// /// Le bloc du cycle se pose là où tombe son premier tome, comme celui d'une série simple : /// l'ordre général du catalogue ne bouge pas. /// [Fact] public void Le_cycle_prend_la_place_du_premier_tome_de_sa_descendance() { List catalogue = [ Livre(1, "Dracula"), Livre(2, "Terre natale"), Livre(3, "Dune"), Livre(4, "L'Éclat de cristal"), ]; List series = [ Serie(1, "Drizzt"), SousSerie(2, "L'Elfe noir", 1, 0, (0, 2)), SousSerie(3, "Les Elfes noirs", 1, 1, (0, 4)), ]; Assert.Collection( GroupementCatalogue.Grouper(catalogue, series), e => Assert.Equal("Dracula", Assert.Single(e.Livres).Titre), e => Assert.Equal("Drizzt", e.Serie!.Titre), e => Assert.Equal("Dune", Assert.Single(e.Livres).Titre)); } /// /// ⚠️ Un niveau qui ne porte rien n'existe pas : ce serait une indentation qui ne se /// replierait sur rien, et un titre de série dont aucun tome n'est au catalogue. /// [Fact] public void Les_series_sans_tome_visible_ne_font_pas_de_niveau() { List series = [ Serie(1, "Cycle"), SousSerie(2, "Vide", 1, 0, (0, 99)), SousSerie(3, "Garnie", 1, 1, (0, 5)), ]; var cycle = Assert.Single(GroupementCatalogue.Grouper([Livre(5, "Un tome")], series)); Assert.Equal("Garnie", Assert.Single(cycle.SousEntrees).Serie!.Titre); } /// /// Une parenté qui boucle en base ne doit pas faire tourner le regroupement sans fin : /// même garde-fou que la remontée du serveur, bornée par le nombre de séries. /// [Fact] public void Une_parente_qui_boucle_ne_bloque_pas_le_catalogue() { List series = [ SousSerie(1, "A", 2, 0, (0, 1)), SousSerie(2, "B", 1, 0), ]; var entrees = GroupementCatalogue.Grouper([Livre(1, "Un livre")], series); Assert.Equal([1], entrees.SelectMany(EntreesEtDescendance).SelectMany(e => e.Livres).Select(l => l.Id)); } private static IEnumerable EntreesEtDescendance(EntreeCatalogue entree) => [entree, .. entree.SousEntrees.SelectMany(EntreesEtDescendance)]; }