using MaBibli.Api.Data;
using MaBibli.Api.Services.Catalogue;
using MaBibli.Shared.Catalogue;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Entites;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

namespace MaBibli.Tests;

/// <summary>
/// CRUD du catalogue, sur une base SQLite <b>en mémoire</b> : même moteur qu'en production,
/// aucun fichier, aucun réseau.
/// </summary>
public class ServiceCatalogueTests : IDisposable
{
    private readonly SqliteConnection _connexion;
    private readonly MaBibliDbContext _db;
    private readonly ServiceCatalogue _service;

    public ServiceCatalogueTests()
    {
        // La connexion doit rester ouverte : SQLite détruit une base « :memory: » à sa fermeture.
        _connexion = new SqliteConnection("Data Source=:memory:");
        _connexion.Open();

        var options = new DbContextOptionsBuilder<MaBibliDbContext>()
            .UseSqlite(_connexion)
            .Options;

        _db = new MaBibliDbContext(options);
        _db.Database.EnsureCreated();

        _service = new ServiceCatalogue(_db, new ServiceAuteurs(_db));
    }

    private static EnregistrementLivre Saisie(
        string titre,
        string? auteur = null,
        string? isbn = null,
        Format format = Format.Physique,
        Statut? statut = null) => new()
    {
        Titre = titre,
        Auteur = auteur,
        Isbn = isbn,
        Format = format,
        Statut = statut,
    };

    [Fact]
    public async Task Creer_enregistre_le_livre_et_trace_lauteur_de_la_saisie()
    {
        var resultat = await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");

        Assert.Null(resultat.Erreur);
        var livre = resultat.Livre!;
        Assert.True(livre.Id > 0);
        Assert.Equal("Germinal", livre.Titre);
        Assert.Equal("mathieu", livre.AjoutePar);
        Assert.Equal(["Émile Zola"], livre.Auteurs.Select(a => a.Nom));
        Assert.NotEqual(default, livre.DateAjout);
    }

    [Fact]
    public async Task Creer_normalise_lisbn()
    {
        var resultat = await _service.CreerAsync(Saisie("Germinal", isbn: "978-2-253-00422-6"), "mathieu");

        Assert.Equal("9782253004226", resultat.Livre!.Isbn);
    }

    [Fact]
    public async Task Creer_refuse_un_titre_vide()
    {
        var resultat = await _service.CreerAsync(Saisie("   "), "mathieu");

        Assert.Null(resultat.Livre);
        Assert.NotNull(resultat.Erreur);
    }

    [Fact]
    public async Task Creer_refuse_un_isbn_invalide_mais_accepte_labsence_disbn()
    {
        var invalide = await _service.CreerAsync(Saisie("Sans titre", isbn: "1234567890123"), "mathieu");
        Assert.Null(invalide.Livre);
        Assert.NotNull(invalide.Erreur);

        // Certains livres n'ont pas d'ISBN : l'absence doit rester parfaitement légitime.
        var sansIsbn = await _service.CreerAsync(Saisie("Carnet de famille"), "mathieu");
        Assert.Null(sansIsbn.Erreur);
        Assert.Null(sansIsbn.Livre!.Isbn);
    }

    [Fact]
    public async Task Le_catalogue_est_unique_physiques_et_numeriques_cohabitent()
    {
        await _service.CreerAsync(Saisie("Papier", format: Format.Physique), "mathieu");
        await _service.CreerAsync(Saisie("Epub", format: Format.Numerique), "mathieu");

        var tout = await _service.ListerAsync(new CritereLivres(), "mathieu");
        Assert.Equal(2, tout.Count);

        var numeriques = await _service.ListerAsync(new CritereLivres { Format = Format.Numerique }, "mathieu");
        Assert.Equal(["Epub"], numeriques.Select(l => l.Titre));
    }

    [Fact]
    public async Task La_recherche_traverse_bien_SQLite()
    {
        await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
        await _service.CreerAsync(Saisie("Le Horla", "Guy de Maupassant"), "mathieu");

        var parAuteur = await _service.ListerAsync(new CritereLivres { Recherche = "maupassant" }, "mathieu");
        Assert.Equal(["Le Horla"], parAuteur.Select(l => l.Titre));

        var parTitre = await _service.ListerAsync(new CritereLivres { Recherche = "GERMI" }, "mathieu");
        Assert.Equal(["Germinal"], parTitre.Select(l => l.Titre));
    }

    /// <summary>
    /// La recherche sans accents doit traverser SQLite, pas seulement LINQ-to-Objects : c'est là
    /// que la colonne normalisée gagne son existence.
    /// </summary>
    [Theory]
    [InlineData("emile")]
    [InlineData("EMILE")]
    [InlineData("Émile Zola")]
    public async Task La_recherche_sans_accents_traverse_bien_SQLite(string terme)
    {
        await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
        await _service.CreerAsync(Saisie("Le Horla", "Guy de Maupassant"), "mathieu");

        var resultat = await _service.ListerAsync(new CritereLivres { Recherche = terme }, "mathieu");

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

    /// <summary>
    /// <b>Invariant central</b> : la bibliothèque est commune. Des livres saisis par des comptes
    /// différents doivent tous ressortir, quelle que soit la personne qui consulte.
    /// </summary>
    [Fact]
    public async Task La_liste_renvoie_les_livres_de_tous_les_utilisateurs()
    {
        await _service.CreerAsync(Saisie("Livre de Mathieu"), "mathieu");
        await _service.CreerAsync(Saisie("Livre de Camille"), "camille");
        await _service.CreerAsync(Saisie("Livre anonyme"), null);

        var tout = await _service.ListerAsync(new CritereLivres(), "mathieu");

        Assert.Equal(3, tout.Count);
        Assert.Contains(tout, l => l.AjoutePar == "mathieu");
        Assert.Contains(tout, l => l.AjoutePar == "camille");
        Assert.Contains(tout, l => l.AjoutePar is null);
    }

    [Fact]
    public async Task Modifier_ne_reecrit_ni_AjoutePar_ni_DateAjout()
    {
        var cree = (await _service.CreerAsync(Saisie("Titre initial", "Auteur Initial"), "camille")).Livre!;

        var modifie = await _service.ModifierAsync(cree.Id, new EnregistrementLivre
        {
            Titre = "Titre corrigé",
            Auteur = "Auteur Corrigé",
            Format = Format.Numerique,
            Statut = Statut.Lu,
        }, "camille");

        Assert.Null(modifie.Erreur);
        Assert.Equal("Titre corrigé", modifie.Livre!.Titre);
        Assert.Equal(Format.Numerique, modifie.Livre.Format);
        Assert.Equal(["Auteur Corrigé"], modifie.Livre.Auteurs.Select(a => a.Nom));

        // La trace de la saisie d'origine survit à l'édition, y compris par quelqu'un d'autre.
        Assert.Equal("camille", modifie.Livre.AjoutePar);
        Assert.Equal(cree.DateAjout, modifie.Livre.DateAjout);
    }

    [Fact]
    public async Task Modifier_un_livre_inexistant_est_signale_comme_introuvable()
    {
        var resultat = await _service.ModifierAsync(4242, Saisie("Fantôme"), "mathieu");

        Assert.True(resultat.EstIntrouvable);
    }

    [Fact]
    public async Task Changer_le_statut_ne_touche_que_le_statut()
    {
        var cree = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;

        var apres = await _service.ChangerStatutAsync(cree.Id, Statut.EnCours, "mathieu");

        Assert.NotNull(apres);
        Assert.Equal(Statut.EnCours, apres!.Statut);
        Assert.Equal(cree.Titre, apres.Titre);
        Assert.Equal(cree.Auteurs.Select(a => a.Nom), apres.Auteurs.Select(a => a.Nom));
        Assert.Equal(cree.AjoutePar, apres.AjoutePar);
    }

    /// <summary>
    /// <b>La décision de cette phase, vérifiée de bout en bout.</b> Un seul livre, deux personnes,
    /// deux statuts qui ne se marchent pas dessus.
    /// </summary>
    [Fact]
    public async Task Deux_utilisateurs_ont_des_statuts_differents_sur_le_meme_livre()
    {
        var livre = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;

        await _service.ChangerStatutAsync(livre.Id, Statut.Lu, "mathieu");
        await _service.ChangerStatutAsync(livre.Id, Statut.EnCours, "camille");

        Assert.Equal(Statut.Lu, (await _service.ObtenirAsync(livre.Id, "mathieu"))!.Statut);
        Assert.Equal(Statut.EnCours, (await _service.ObtenirAsync(livre.Id, "camille"))!.Statut);

        // Une troisième personne n'a rien posé : pour elle le livre est « non commencé ».
        Assert.Null((await _service.ObtenirAsync(livre.Id, "claire"))!.Statut);

        // Une seule ligne par personne : rien ne s'empile à force de changer d'avis.
        Assert.Equal(2, await _db.StatutsLecture.CountAsync(s => s.LivreId == livre.Id));
    }

    [Fact]
    public async Task Un_livre_cree_sans_statut_nen_ecrit_aucun()
    {
        var livre = (await _service.CreerAsync(Saisie("Germinal"), "mathieu")).Livre!;

        Assert.Null((await _service.ObtenirAsync(livre.Id, "mathieu"))!.Statut);
        Assert.Empty(_db.StatutsLecture);
    }

    [Fact]
    public async Task Un_statut_nul_remet_le_livre_a_non_commence()
    {
        var livre = (await _service.CreerAsync(Saisie("Germinal", statut: Statut.Lu), "mathieu")).Livre!;
        Assert.Equal(Statut.Lu, livre.Statut);

        var apres = await _service.ChangerStatutAsync(livre.Id, null, "mathieu");

        Assert.Null(apres!.Statut);
        Assert.Empty(_db.StatutsLecture);
    }

    /// <summary>Éditer un livre pose MON statut, jamais celui de la personne qui l'a saisi.</summary>
    [Fact]
    public async Task Modifier_un_livre_ne_touche_pas_au_statut_des_autres()
    {
        var livre = (await _service.CreerAsync(Saisie("Germinal", statut: Statut.Lu), "mathieu")).Livre!;

        await _service.ModifierAsync(
            livre.Id, Saisie("Germinal", statut: Statut.EnCours), "camille");

        Assert.Equal(Statut.Lu, (await _service.ObtenirAsync(livre.Id, "mathieu"))!.Statut);
        Assert.Equal(Statut.EnCours, (await _service.ObtenirAsync(livre.Id, "camille"))!.Statut);
    }

    [Fact]
    public async Task Changer_le_statut_dun_livre_inexistant_renvoie_null()
        => Assert.Null(await _service.ChangerStatutAsync(4242, Statut.Lu, "mathieu"));

    [Fact]
    public async Task Sans_identite_aucun_statut_nest_ecrit()
    {
        var livre = (await _service.CreerAsync(Saisie("Anonyme", statut: Statut.Lu), null)).Livre!;

        Assert.Null(livre.Statut);
        Assert.Empty(_db.StatutsLecture);
    }

    [Fact]
    public async Task Un_livre_peut_avoir_plusieurs_auteurs_dans_lordre()
    {
        var livre = (await _service.CreerAsync(
            Saisie("Introduction to Algorithms",
                "Thomas H. Cormen; Charles E. Leiserson; Ronald L. Rivest; Clifford Stein"),
            "mathieu")).Livre!;

        Assert.Equal(
            ["Thomas H. Cormen", "Charles E. Leiserson", "Ronald L. Rivest", "Clifford Stein"],
            livre.Auteurs.Select(a => a.Nom));
    }

    [Fact]
    public async Task Les_variantes_sures_dun_auteur_partagent_la_meme_fiche()
    {
        var a = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
        var b = (await _service.CreerAsync(Saisie("Nana", "Zola, Emile"), "mathieu")).Livre!;
        var c = (await _service.CreerAsync(Saisie("La Curée", "E. Zola"), "mathieu")).Livre!;

        var id = a.Auteurs.Single().Id;
        Assert.Equal(id, b.Auteurs.Single().Id);
        Assert.Equal(id, c.Auteurs.Single().Id);
        Assert.Equal(1, await _db.Auteurs.CountAsync());
    }

    [Fact]
    public async Task Un_patronyme_seul_ne_se_fond_pas_dans_un_nom_complet()
    {
        var complet = (await _service.CreerAsync(
            Saisie("Pandora's Star", "Peter F. Hamilton"), "mathieu")).Livre!;
        var seul = (await _service.CreerAsync(Saisie("Autre livre", "Hamilton"), "mathieu")).Livre!;

        // Deux fiches distinctes : le rapprochement est ambigu, il sera proposé, pas appliqué.
        Assert.NotEqual(complet.Auteurs.Single().Id, seul.Auteurs.Single().Id);
        Assert.Equal(2, await _db.Auteurs.CountAsync());
    }

    [Fact]
    public async Task Supprimer_retire_le_livre_du_catalogue()
    {
        var cree = (await _service.CreerAsync(Saisie("À jeter", "Auteur Jetable"), "mathieu")).Livre!;

        Assert.True(await _service.SupprimerAsync(cree.Id));
        Assert.Null(await _service.ObtenirAsync(cree.Id, "mathieu"));
        Assert.Empty(await _service.ListerAsync(new CritereLivres(), "mathieu"));

        // L'auteur n'a plus aucun livre : il ne doit pas rester à polluer l'écran des auteurs.
        Assert.Empty(_db.Auteurs);

        // Seconde suppression : plus rien à supprimer.
        Assert.False(await _service.SupprimerAsync(cree.Id));
    }

    [Fact]
    public async Task Un_auteur_encore_lu_par_un_autre_livre_survit_a_une_suppression()
    {
        var premier = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
        await _service.CreerAsync(Saisie("Nana", "Émile Zola"), "mathieu");

        await _service.SupprimerAsync(premier.Id);

        Assert.Equal(1, await _db.Auteurs.CountAsync());
    }

    [Fact]
    public async Task Obtenir_renvoie_la_fiche_complete()
    {
        var cree = (await _service.CreerAsync(
            Saisie("Le Horla", "Guy de Maupassant", "9782080704092"), "mathieu")).Livre!;

        var relu = await _service.ObtenirAsync(cree.Id, "mathieu");

        Assert.NotNull(relu);
        Assert.Equal("Le Horla", relu!.Titre);
        Assert.Equal("9782080704092", relu.Isbn);
        Assert.Equal(["Guy de Maupassant"], relu.Auteurs.Select(a => a.Nom));
    }

    public void Dispose()
    {
        _db.Dispose();
        _connexion.Dispose();
        GC.SuppressFinalize(this);
    }
}
