using MaBibli.Shared.Catalogue;
using MaBibli.Shared.Entites;

namespace MaBibli.Tests;

/// <summary>
/// Filtrage et recherche du catalogue. Purement en mémoire : ni base, ni réseau.
/// </summary>
public class FiltreLivresTests
{
    private static Auteur Auteur(int id, string nom)
    {
        var auteur = new Auteur { Id = id, Nom = nom };
        auteur.RecalculerFormes();
        return auteur;
    }

    private static readonly Auteur Zola = Auteur(1, "Émile Zola");
    private static readonly Auteur Maupassant = Auteur(2, "Guy de Maupassant");

    private static Livre Livre(
        int id, string titre, Auteur? auteur, Format format, Statut? statut, string? ajoutePar,
        string? lecteur = "mathieu")
    {
        var livre = new Livre
        {
            Id = id,
            Titre = titre,
            Format = format,
            AjoutePar = ajoutePar,
        };

        livre.RecalculerFormes();

        if (auteur is not null)
        {
            livre.Auteurs.Add(new LivreAuteur { LivreId = id, AuteurId = auteur.Id, Auteur = auteur });
        }

        if (statut is { } valeur && lecteur is not null)
        {
            livre.Statuts.Add(new StatutLecture { LivreId = id, Utilisateur = lecteur, Statut = valeur });
        }

        return livre;
    }

    // « mathieu » a un statut sur les quatre livres ; « camille » n'en a aucun.
    private static readonly Livre[] Catalogue =
    [
        Livre(1, "Germinal", Zola, Format.Physique, Statut.Lu, "mathieu"),
        Livre(2, "La Bête humaine", Zola, Format.Numerique, Statut.ALire, "camille"),
        Livre(3, "Le Horla", Maupassant, Format.Physique, Statut.EnCours, "camille"),
        Livre(4, "Bel-Ami", null, Format.Numerique, Statut.Lu, null),
    ];

    private static List<Livre> Filtrer(CritereLivres criteres, string? utilisateur = "mathieu") =>
        FiltreLivres.Appliquer(Catalogue.AsQueryable(), criteres, utilisateur).ToList();

    [Fact]
    public void Sans_critere_tout_le_catalogue_remonte()
        => Assert.Equal(4, Filtrer(new CritereLivres()).Count);

    [Fact]
    public void Le_tri_est_alphabetique_sur_le_titre()
        => Assert.Equal(
            ["Bel-Ami", "Germinal", "La Bête humaine", "Le Horla"],
            Filtrer(new CritereLivres()).Select(l => l.Titre));

    [Theory]
    [InlineData(Format.Physique, 2)]
    [InlineData(Format.Numerique, 2)]
    public void Le_filtre_format_ne_garde_que_ce_format(Format format, int attendu)
    {
        var resultat = Filtrer(new CritereLivres { Format = format });

        Assert.Equal(attendu, resultat.Count);
        Assert.All(resultat, l => Assert.Equal(format, l.Format));
    }

    [Theory]
    [InlineData(Statut.ALire, 1)]
    [InlineData(Statut.EnCours, 1)]
    [InlineData(Statut.Lu, 2)]
    public void Le_filtre_statut_ne_garde_que_ce_statut(Statut statut, int attendu)
        => Assert.Equal(attendu, Filtrer(new CritereLivres { Statut = statut }).Count);

    /// <summary>
    /// <b>Le statut est personnel.</b> Le même filtre, la même bibliothèque, une autre personne :
    /// rien ne remonte, parce que « camille » n'a posé aucun statut.
    /// </summary>
    [Fact]
    public void Le_filtre_statut_ne_voit_que_le_statut_de_lappelant()
    {
        Assert.Equal(2, Filtrer(new CritereLivres { Statut = Statut.Lu }, "mathieu").Count);
        Assert.Empty(Filtrer(new CritereLivres { Statut = Statut.Lu }, "camille"));

        // Sans filtre, en revanche, camille voit toute la bibliothèque : elle est commune.
        Assert.Equal(4, Filtrer(new CritereLivres(), "camille").Count);
    }

    [Fact]
    public void Sans_identite_aucun_statut_ne_correspond()
        => Assert.Empty(Filtrer(new CritereLivres { Statut = Statut.Lu }, null));

    [Fact]
    public void La_recherche_porte_sur_le_titre()
        => Assert.Equal(["Germinal"], Filtrer(new CritereLivres { Recherche = "germ" }).Select(l => l.Titre));

    [Fact]
    public void La_recherche_porte_aussi_sur_lauteur()
        => Assert.Equal(
            ["Germinal", "La Bête humaine"],
            Filtrer(new CritereLivres { Recherche = "zola" }).Select(l => l.Titre));

    /// <summary>
    /// Le besoin exprimé dans IDEES.md : « Emile » doit trouver « Émile ». Il n'y a pas de
    /// collation SQLite pour ça, seule la colonne normalisée le permet.
    /// </summary>
    [Theory]
    [InlineData("emile")]
    [InlineData("Émile")]
    [InlineData("EMILE ZOLA")]
    [InlineData("zola, émile")]
    public void La_recherche_ignore_les_accents_sur_lauteur(string terme)
        => Assert.Equal(
            ["Germinal", "La Bête humaine"],
            Filtrer(new CritereLivres { Recherche = terme }).Select(l => l.Titre));

    [Theory]
    [InlineData("bete")]
    [InlineData("BÊTE")]
    public void La_recherche_ignore_les_accents_sur_le_titre(string terme)
        => Assert.Equal(["La Bête humaine"], Filtrer(new CritereLivres { Recherche = terme }).Select(l => l.Titre));

    [Theory]
    [InlineData("HORLA")]
    [InlineData("horla")]
    [InlineData("  Horla  ")]
    public void La_recherche_ignore_la_casse_et_les_espaces_autour(string terme)
        => Assert.Single(Filtrer(new CritereLivres { Recherche = terme }));

    [Fact]
    public void Un_livre_sans_auteur_ne_fait_pas_echouer_la_recherche()
    {
        // Le livre 4 n'a aucun auteur : la clause sur les auteurs doit le traverser sans exploser.
        var resultat = Filtrer(new CritereLivres { Recherche = "bel" });

        Assert.Equal(["Bel-Ami"], resultat.Select(l => l.Titre));
    }

    [Fact]
    public void Le_filtre_par_auteur_regroupe_ses_livres()
        => Assert.Equal(
            ["Germinal", "La Bête humaine"],
            Filtrer(new CritereLivres { AuteurId = Zola.Id }).Select(l => l.Titre));

    [Fact]
    public void Les_criteres_se_combinent()
    {
        var resultat = Filtrer(new CritereLivres
        {
            Recherche = "zola",
            Format = Format.Physique,
            Statut = Statut.Lu,
        });

        Assert.Equal(["Germinal"], resultat.Select(l => l.Titre));
    }

    [Fact]
    public void Une_recherche_vide_equivaut_a_aucune_recherche()
    {
        Assert.Equal(4, Filtrer(new CritereLivres { Recherche = "   " }).Count);
        Assert.Equal(4, Filtrer(new CritereLivres { Recherche = "" }).Count);
    }

    /// <summary>
    /// <b>Invariant le plus important du projet.</b> La bibliothèque est commune au foyer :
    /// une lecture ne doit JAMAIS être restreinte à ce qu'a saisi l'utilisateur courant.
    /// Ce test échouerait au moindre filtre ajouté sur <c>AjoutePar</c>.
    /// </summary>
    [Fact]
    public void La_lecture_ne_filtre_jamais_sur_AjoutePar()
    {
        var resultat = Filtrer(new CritereLivres());

        // Les livres des trois provenances (deux comptes + un sans trace) sont tous là.
        Assert.Contains(resultat, l => l.AjoutePar == "mathieu");
        Assert.Contains(resultat, l => l.AjoutePar == "camille");
        Assert.Contains(resultat, l => l.AjoutePar is null);
        Assert.Equal(Catalogue.Length, resultat.Count);
    }

    /// <summary>
    /// Second verrou sur le même invariant, par le type : aucun critère de consultation ne doit
    /// permettre d'exprimer un cloisonnement par utilisateur. Si quelqu'un ajoute une propriété
    /// « AjoutePar » à <see cref="CritereLivres"/>, ce test le lui rappelle.
    /// </summary>
    [Fact]
    public void Les_criteres_nexposent_aucun_champ_dutilisateur()
    {
        var proprietes = typeof(CritereLivres)
            .GetProperties()
            .Select(p => p.Name)
            .ToArray();

        Assert.DoesNotContain(proprietes, nom => nom.Contains("Ajoute", StringComparison.OrdinalIgnoreCase));
        Assert.DoesNotContain(proprietes, nom => nom.Contains("Utilisateur", StringComparison.OrdinalIgnoreCase));
    }

    /// <summary>
    /// Troisième verrou, sur la décision inverse : <see cref="Livre"/> ne doit plus jamais porter
    /// de colonne <c>Statut</c>, qui redeviendrait commune à tout le foyer.
    /// </summary>
    [Fact]
    public void Livre_ne_porte_aucun_statut_commun()
    {
        var statut = typeof(Livre).GetProperty("Statut");

        Assert.Null(statut);
    }
}
