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

namespace MaBibli.Tests;

/// <summary>
/// Prêts, sur une base SQLite <b>en mémoire</b> : même moteur qu'en production — ce qui compte
/// ici, l'index unique partiel « un seul prêt ouvert par livre » n'existant que côté base.
/// </summary>
public class ServicePretsTests : IDisposable
{
    private readonly SqliteConnection _connexion;
    private readonly MaBibliDbContext _db;
    private readonly ServicePrets _prets;
    private readonly ServiceCatalogue _catalogue;

    public ServicePretsTests()
    {
        _connexion = new SqliteConnection("Data Source=:memory:");
        _connexion.Open();

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

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

        _prets = new ServicePrets(_db);
        _catalogue = new ServiceCatalogue(_db, new ServiceAuteurs(_db));
    }

    private async Task<int> LivreAsync(string titre = "Germinal", Format format = Format.Physique)
    {
        var resultat = await _catalogue.CreerAsync(
            new EnregistrementLivre { Titre = titre, Auteur = "Émile Zola", Format = format },
            "mathieu");

        return resultat.Livre!.Id;
    }

    [Fact]
    public async Task Preter_ouvre_un_pret_sans_date_de_retour()
    {
        var livreId = await LivreAsync();

        var resultat = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" });

        Assert.Null(resultat.Erreur);
        var pret = resultat.Pret!;
        Assert.Equal("Paul", pret.Emprunteur);
        Assert.Null(pret.DateRetour);
        Assert.True(pret.EstEnCours);

        // Le livre est recopié à plat pour que la vue « prêts en cours » n'ait qu'un appel à faire.
        Assert.Equal("Germinal", pret.LivreTitre);
        Assert.Equal("Émile Zola", pret.LivreAuteurs);
    }

    [Fact]
    public async Task Preter_ecarte_les_espaces_autour_de_lemprunteur()
    {
        var livreId = await LivreAsync();

        var resultat = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "  Paul  " });

        Assert.Equal("Paul", resultat.Pret!.Emprunteur);
    }

    [Fact]
    public async Task Preter_refuse_un_emprunteur_vide()
    {
        var livreId = await LivreAsync();

        var resultat = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "   " });

        Assert.NotNull(resultat.Erreur);
        Assert.Empty(await _db.Prets.ToListAsync());
    }

    [Fact]
    public async Task Preter_refuse_un_livre_inconnu()
    {
        var resultat = await _prets.PreterAsync(999, new EnregistrementPret { Emprunteur = "Paul" });

        Assert.True(resultat.EstIntrouvable);
    }

    /// <summary>
    /// CLAUDE.md : un ebook n'est qu'une fiche, il n'y a pas d'exemplaire à confier. La règle est
    /// tenue par l'API, pas seulement par l'interface.
    /// </summary>
    [Fact]
    public async Task Preter_refuse_un_livre_numerique()
    {
        var livreId = await LivreAsync("Effective Java", Format.Numerique);

        var resultat = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" });

        Assert.NotNull(resultat.Erreur);
        Assert.Contains("numérique", resultat.Erreur);
        Assert.Empty(await _db.Prets.ToListAsync());
    }

    [Fact]
    public async Task Preter_refuse_un_livre_deja_sorti_et_nomme_qui_la()
    {
        var livreId = await LivreAsync();
        await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" });

        var resultat = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Marie" });

        Assert.NotNull(resultat.Erreur);
        Assert.Contains("Paul", resultat.Erreur);
        Assert.Single(await _db.Prets.ToListAsync());
    }

    /// <summary>
    /// Le service vérifie déjà le doublon, mais entre sa vérification et son insertion il y a une
    /// fenêtre. Ce test contourne volontairement le service pour prouver que la base refuse aussi.
    /// </summary>
    [Fact]
    public async Task La_base_refuse_deux_prets_ouverts_sur_le_meme_livre()
    {
        var livreId = await LivreAsync();
        await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" });

        _db.Prets.Add(new Pret
        {
            LivreId = livreId,
            Emprunteur = "Marie",
            DatePret = DateTime.UtcNow,
            DateRetour = null,
        });

        await Assert.ThrowsAsync<DbUpdateException>(() => _db.SaveChangesAsync());
    }

    [Fact]
    public async Task Preter_accepte_une_date_passee()
    {
        var livreId = await LivreAsync();
        var noel = new DateTime(2025, 12, 25, 0, 0, 0, DateTimeKind.Utc);

        var resultat = await _prets.PreterAsync(
            livreId, new EnregistrementPret { Emprunteur = "Paul", DatePret = noel });

        Assert.Null(resultat.Erreur);
        Assert.Equal(noel, resultat.Pret!.DatePret);
    }

    [Fact]
    public async Task Preter_refuse_une_date_future()
    {
        var livreId = await LivreAsync();

        var resultat = await _prets.PreterAsync(
            livreId,
            new EnregistrementPret { Emprunteur = "Paul", DatePret = DateTime.UtcNow.AddDays(30) });

        Assert.NotNull(resultat.Erreur);
    }

    [Fact]
    public async Task Rendre_pose_la_date_sans_supprimer_le_pret()
    {
        var livreId = await LivreAsync();
        var pret = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!;

        var resultat = await _prets.RendreAsync(pret.Id, new RetourPret());

        Assert.Null(resultat.Erreur);
        Assert.NotNull(resultat.Pret!.DateRetour);
        Assert.False(resultat.Pret.EstEnCours);

        // La ligne est toujours là : c'est toute la raison d'être d'une table séparée.
        Assert.Single(await _db.Prets.ToListAsync());
    }

    [Fact]
    public async Task Rendre_refuse_un_pret_deja_clos()
    {
        var livreId = await LivreAsync();
        var pret = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!;
        await _prets.RendreAsync(pret.Id, new RetourPret());

        var resultat = await _prets.RendreAsync(pret.Id, new RetourPret());

        Assert.NotNull(resultat.Erreur);
    }

    [Fact]
    public async Task Rendre_refuse_un_pret_inconnu()
    {
        Assert.True((await _prets.RendreAsync(999, new RetourPret())).EstIntrouvable);
    }

    [Fact]
    public async Task Rendre_refuse_une_date_anterieure_au_pret()
    {
        var livreId = await LivreAsync();
        var pret = (await _prets.PreterAsync(
            livreId,
            new EnregistrementPret { Emprunteur = "Paul", DatePret = DateTime.UtcNow.AddDays(-3) })).Pret!;

        var resultat = await _prets.RendreAsync(
            pret.Id, new RetourPret { DateRetour = DateTime.UtcNow.AddDays(-10) });

        Assert.NotNull(resultat.Erreur);
    }

    [Fact]
    public async Task Un_livre_rendu_peut_etre_prete_a_quelquun_dautre()
    {
        var livreId = await LivreAsync();
        var premier = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!;
        await _prets.RendreAsync(premier.Id, new RetourPret());

        var second = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Marie" });

        Assert.Null(second.Erreur);

        var historique = await _prets.HistoriqueAsync(livreId);
        Assert.Equal(2, historique!.Count);

        // Le prêt clos n'a pas bougé : l'historique conserve bien qui avait le livre avant.
        Assert.Contains(historique, p => p.Emprunteur == "Paul" && !p.EstEnCours);
        Assert.Contains(historique, p => p.Emprunteur == "Marie" && p.EstEnCours);
    }

    [Fact]
    public async Task Historique_va_du_plus_recent_au_plus_ancien()
    {
        var livreId = await LivreAsync();

        foreach (var (nom, jours) in new[] { ("Paul", -90), ("Marie", -30) })
        {
            var pret = (await _prets.PreterAsync(
                livreId,
                new EnregistrementPret { Emprunteur = nom, DatePret = DateTime.UtcNow.AddDays(jours) })).Pret!;

            await _prets.RendreAsync(pret.Id, new RetourPret { DateRetour = DateTime.UtcNow.AddDays(jours + 10) });
        }

        var historique = await _prets.HistoriqueAsync(livreId);

        Assert.Equal(["Marie", "Paul"], historique!.Select(p => p.Emprunteur));
    }

    [Fact]
    public async Task Historique_distingue_livre_inconnu_et_livre_jamais_prete()
    {
        var livreId = await LivreAsync();

        Assert.Null(await _prets.HistoriqueAsync(999));
        Assert.Empty((await _prets.HistoriqueAsync(livreId))!);
    }

    [Fact]
    public async Task EnCours_ne_liste_que_les_livres_dehors_du_plus_ancien_au_plus_recent()
    {
        var germinal = await LivreAsync("Germinal");
        var horla = await LivreAsync("Le Horla");
        var petitPrince = await LivreAsync("Le Petit Prince");

        // Rendu : ne doit plus apparaître.
        var rendu = (await _prets.PreterAsync(germinal, new EnregistrementPret { Emprunteur = "Paul" })).Pret!;
        await _prets.RendreAsync(rendu.Id, new RetourPret());

        await _prets.PreterAsync(
            horla, new EnregistrementPret { Emprunteur = "Marie", DatePret = DateTime.UtcNow.AddDays(-2) });
        await _prets.PreterAsync(
            petitPrince, new EnregistrementPret { Emprunteur = "Jean", DatePret = DateTime.UtcNow.AddDays(-200) });

        var enCours = await _prets.EnCoursAsync();

        // Le plus ancien d'abord : c'est le livre oublié qu'on cherche dans cette vue.
        Assert.Equal(["Jean", "Marie"], enCours.Select(p => p.Emprunteur));
        Assert.DoesNotContain(enCours, p => p.LivreId == germinal);
    }

    /// <summary>
    /// Le catalogue doit signaler qu'un livre est dehors — et cette information est <b>commune</b>
    /// au foyer, contrairement au statut de lecture.
    /// </summary>
    [Fact]
    public async Task Le_catalogue_signale_un_livre_prete_a_tout_le_monde()
    {
        var livreId = await LivreAsync();
        await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" });

        foreach (var utilisateur in new[] { "mathieu", "quelquun-dautre" })
        {
            var livre = await _catalogue.ObtenirAsync(livreId, utilisateur);
            Assert.Equal("Paul", livre!.PreteA);
            Assert.True(livre.EstPrete);
            Assert.NotNull(livre.PreteDepuis);

            var liste = await _catalogue.ListerAsync(new CritereLivres(), utilisateur);
            Assert.Equal("Paul", liste.Single().PreteA);
        }
    }

    [Fact]
    public async Task Le_catalogue_ne_signale_plus_rien_une_fois_le_livre_rendu()
    {
        var livreId = await LivreAsync();
        var pret = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!;
        await _prets.RendreAsync(pret.Id, new RetourPret());

        var livre = await _catalogue.ObtenirAsync(livreId, "mathieu");

        Assert.Null(livre!.PreteA);
        Assert.False(livre.EstPrete);
    }

    /// <summary>
    /// Choix assumé : supprimer un livre emporte ses prêts. Un historique orphelin — « quelqu'un
    /// a emprunté quelque chose » — ne se lit plus. La cascade était déjà déclarée sur la relation,
    /// ce test la fige pour qu'un changement de comportement ne passe pas inaperçu.
    /// </summary>
    [Fact]
    public async Task Supprimer_un_livre_emporte_son_historique_de_prets()
    {
        var livreId = await LivreAsync();
        var pret = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!;
        await _prets.RendreAsync(pret.Id, new RetourPret());
        await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Marie" });

        Assert.True(await _catalogue.SupprimerAsync(livreId));

        Assert.Empty(await _db.Prets.ToListAsync());
        Assert.Empty(await _prets.EnCoursAsync());
    }

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