using MaBibli.Client.Services;
using MaBibli.Shared.Catalogue;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Entites;

namespace MaBibli.Tests;

/// <summary>
/// Recherche hors-ligne : le filtre appliqué dans le navigateur à l'instantané IndexedDB.
/// </summary>
/// <remarks>
/// L'essentiel de ces tests confronte <see cref="FiltreLivresLocal"/> à
/// <see cref="FiltreLivres"/>, celui qui s'exécute côté base. Ce sont deux implémentations
/// distinctes — l'une sur des entités aux colonnes normalisées, l'autre sur des DTO qui n'en ont
/// pas — et rien n'empêcherait leurs comportements de diverger en silence. Une bibliothèque qui
/// ne se cherche pas de la même façon selon qu'on a du réseau ou non serait pire qu'un cache
/// absent : l'utilisateur conclurait que le livre n'est pas dans sa bibliothèque.
/// </remarks>
public class FiltreLivresLocalTests
{
    private const string Lecteur = "mathieu";

    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)
    {
        var livre = new Livre { Id = id, Titre = titre, Format = format, AjoutePar = Lecteur };
        livre.RecalculerFormes();

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

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

        return livre;
    }

    private static readonly Livre[] Entites =
    [
        Livre(1, "Germinal", Zola, Format.Physique, Statut.Lu),
        Livre(2, "La Bête humaine", Zola, Format.Numerique, Statut.ALire),
        Livre(3, "Le Horla", Maupassant, Format.Physique, Statut.EnCours),
        Livre(4, "Bel-Ami", Maupassant, Format.Numerique, null),
        Livre(5, "Œuvres complètes", Zola, Format.Physique, Statut.ALire),
        Livre(6, "L'Éducation sentimentale", null, Format.Physique, null),
    ];

    /// <summary>
    /// Ce que l'API renvoie, et donc ce qui est rangé dans IndexedDB : le statut y est déjà
    /// résolu pour l'utilisateur courant, le client n'a plus personne à désigner.
    /// </summary>
    private static readonly LivreDto[] Instantane = Entites
        .Select(l => new LivreDto
        {
            Id = l.Id,
            Titre = l.Titre,
            Format = l.Format,
            DateAjout = DateTime.UtcNow,
            Statut = l.Statuts.FirstOrDefault(s => s.Utilisateur == Lecteur)?.Statut,
            Auteurs = l.Auteurs
                .Select(la => new AuteurDto { Id = la.AuteurId, Nom = la.Auteur!.Nom })
                .ToList(),
        })
        .ToArray();

    public static TheoryData<CritereLivres> Criteres =>
    [
        new CritereLivres(),
        new CritereLivres { Recherche = "germinal" },
        new CritereLivres { Recherche = "GERMINAL" },
        new CritereLivres { Recherche = "emile" },        // sans accent → doit trouver « Émile »
        new CritereLivres { Recherche = "Émile" },
        new CritereLivres { Recherche = "zola emile" },   // ordre inversé → clé de regroupement
        new CritereLivres { Recherche = "bete" },
        new CritereLivres { Recherche = "oeuvres" },
        new CritereLivres { Recherche = "introuvable" },
        new CritereLivres { Format = Format.Physique },
        new CritereLivres { Format = Format.Numerique },
        new CritereLivres { Statut = Statut.Lu },
        new CritereLivres { Statut = Statut.ALire },
        new CritereLivres { AuteurId = 1 },
        new CritereLivres { AuteurId = 2 },
        new CritereLivres { AuteurId = 99 },
        new CritereLivres { Recherche = "zola", Format = Format.Physique, Statut = Statut.ALire },
    ];

    [Theory]
    [MemberData(nameof(Criteres))]
    public void Le_filtre_hors_ligne_rend_exactement_ce_que_rend_le_serveur(CritereLivres criteres)
    {
        var serveur = FiltreLivres.Appliquer(Entites.AsQueryable(), criteres, Lecteur)
            .Select(l => l.Id)
            .ToList();

        var local = FiltreLivresLocal.Appliquer(Instantane, criteres)
            .Select(l => l.Id)
            .ToList();

        // Séquences comparées, pas ensembles : le tri fait partie du contrat.
        Assert.Equal(serveur, local);
    }

    [Fact]
    public void Le_catalogue_entier_est_cherchable_pas_seulement_ce_qui_etait_affiche()
    {
        // Le cœur de la décision « IndexedDB plutôt que cache du service worker » : une recherche
        // jamais tapée en ligne doit rendre un résultat hors-ligne.
        var resultat = FiltreLivresLocal.Appliquer(Instantane, new CritereLivres { Recherche = "horla" });

        Assert.Equal(3, Assert.Single(resultat).Id);
    }

    [Fact]
    public void La_recherche_ignore_les_accents_dans_les_deux_sens()
    {
        Assert.Single(FiltreLivresLocal.Appliquer(Instantane, new CritereLivres { Recherche = "bete humaine" }));
        Assert.Single(FiltreLivresLocal.Appliquer(Instantane, new CritereLivres { Recherche = "Bête humaine" }));
        Assert.Single(FiltreLivresLocal.Appliquer(Instantane, new CritereLivres { Recherche = "education" }));
    }

    [Fact]
    public void Le_tri_est_alphabetique_et_insensible_aux_accents()
        => Assert.Equal(
            ["Bel-Ami", "Germinal", "L'Éducation sentimentale", "La Bête humaine", "Le Horla", "Œuvres complètes"],
            FiltreLivresLocal.Appliquer(Instantane, new CritereLivres()).Select(l => l.Titre));

    [Fact]
    public void Un_livre_sans_statut_ne_remonte_sous_aucun_statut_mais_reste_visible()
    {
        Assert.DoesNotContain(
            FiltreLivresLocal.Appliquer(Instantane, new CritereLivres { Statut = Statut.Lu }),
            l => l.Id == 4);

        Assert.Contains(
            FiltreLivresLocal.Appliquer(Instantane, new CritereLivres()),
            l => l.Id == 4);
    }

    [Fact]
    public void Un_instantane_vide_ne_fait_pas_echouer_la_recherche()
        => Assert.Empty(FiltreLivresLocal.Appliquer([], new CritereLivres { Recherche = "zola" }));
}
