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

namespace MaBibli.Tests;

/// <summary>
/// Sagas, cycles et séries, sur une base SQLite <b>en mémoire</b> : les invariants tenus ici
/// sont pour moitié des index (unicité du titre, un livre une seule fois par série) et ne se
/// vérifieraient pas sur un faux dépôt.
/// </summary>
/// <remarks>
/// Ce qui est protégé en priorité : <b>un tome sans livre est normal</b> (c'est le trou qu'on
/// vient voir), une suppression de livre <b>laisse</b> le trou, et l'ordre de lecture est
/// stocké — jamais déduit.
/// </remarks>
public class ServiceSeriesTests : IDisposable
{
    private readonly SqliteConnection _connexion;
    private readonly MaBibliDbContext _db;
    private readonly ServiceSeries _series;
    private readonly ServiceCatalogue _catalogue;
    private readonly ServiceSouhaits _souhaits;

    private const string Mathieu = "mathieu";
    private const string Camille = "camille";

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

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

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

        _souhaits = new ServiceSouhaits(_db);
        _series = new ServiceSeries(_db, _souhaits);
        _catalogue = new ServiceCatalogue(_db, new ServiceAuteurs(_db));
    }

    private async Task<int> LivreAsync(string titre, string auteur = "R. A. Salvatore")
    {
        var resultat = await _catalogue.CreerAsync(
            new EnregistrementLivre { Titre = titre, Auteur = auteur }, Mathieu, confirmerDoublon: true);

        return resultat.Livre!.Id;
    }

    private async Task<SerieDto> SerieAsync(string titre, int? parenteId = null)
    {
        var resultat = await _series.CreerAsync(
            new EnregistrementSerie { Titre = titre, SerieParenteId = parenteId }, Mathieu);

        Assert.Null(resultat.Erreur);
        return resultat.Serie!;
    }

    [Fact]
    public async Task Une_serie_melange_tomes_possedes_et_tomes_manquants()
    {
        var serie = await SerieAsync("L'Elfe noir");
        var livreId = await LivreAsync("Terre natale");

        await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { LivreId = livreId });
        var apres = (await _series.AjouterElementAsync(
            serie.Id, new AjoutElementSerie { Titre = "Terre d'exil" })).Serie!;

        Assert.Equal(["Terre natale", "Terre d'exil"], apres.Elements.Select(e => e.Titre));
        Assert.Equal([true, false], apres.Elements.Select(e => e.Possede));

        // Le compteur de l'écran : « 1 sur 2 ».
        Assert.Equal(1, apres.NombrePossedes);
    }

    [Fact]
    public async Task Un_tome_sans_titre_ni_livre_est_refuse()
    {
        var serie = await SerieAsync("L'Elfe noir");

        var resultat = await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie());

        Assert.NotNull(resultat.Erreur);
    }

    [Fact]
    public async Task Supprimer_un_livre_laisse_un_trou_nomme_dans_la_serie()
    {
        var serie = await SerieAsync("L'Elfe noir");
        var livreId = await LivreAsync("Terre natale");
        await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { LivreId = livreId });

        await _catalogue.SupprimerAsync(livreId);

        // ⚠️ L'invariant décisif : la place survit, avec son titre. Une cascade effacerait le
        // tome 1 de la saga parce qu'on a perdu son exemplaire.
        var element = (await _series.ListerAsync()).Single().Elements.Single();
        Assert.Equal("Terre natale", element.Titre);
        Assert.False(element.Possede);
    }

    [Fact]
    public async Task Lordre_de_lecture_est_stocke_et_se_reordonne()
    {
        var serie = await SerieAsync("L'Elfe noir");
        var avant = serie;

        foreach (var titre in new[] { "Terre natale", "Terre d'exil", "Terre promise" })
        {
            avant = (await _series.AjouterElementAsync(
                serie.Id, new AjoutElementSerie { Titre = titre })).Serie!;
        }

        var inverse = avant.Elements.Select(e => e.Id).Reverse().ToList();
        var apres = (await _series.ReordonnerAsync(serie.Id, inverse)).Serie!;

        Assert.Equal(["Terre promise", "Terre d'exil", "Terre natale"], apres.Elements.Select(e => e.Titre));
        Assert.Equal([0, 1, 2], apres.Elements.Select(e => e.Position));
    }

    [Fact]
    public async Task Un_tome_absent_de_lordre_recu_est_conserve_a_la_suite()
    {
        var serie = await SerieAsync("L'Elfe noir");
        var un = (await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { Titre = "Un" })).Serie!;
        await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { Titre = "Deux" });

        // Le client ne connaissait que le premier tome : le second ne doit pas disparaître.
        var apres = (await _series.ReordonnerAsync(serie.Id, [un.Elements[0].Id])).Serie!;

        Assert.Equal(["Un", "Deux"], apres.Elements.Select(e => e.Titre));
    }

    [Fact]
    public async Task Un_meme_livre_ne_tient_quune_place_dans_une_serie_mais_vit_dans_plusieurs()
    {
        var serie = await SerieAsync("L'Elfe noir");
        var autre = await SerieAsync("Les Royaumes oubliés");
        var livreId = await LivreAsync("Terre natale");

        await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { LivreId = livreId });

        var doublon = await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { LivreId = livreId });
        var ailleurs = await _series.AjouterElementAsync(autre.Id, new AjoutElementSerie { LivreId = livreId });

        Assert.NotNull(doublon.Erreur);

        // Un livre appartient légitimement à plusieurs regroupements : une série, un cycle,
        // une intégrale.
        Assert.Null(ailleurs.Erreur);
    }

    [Fact]
    public async Task Une_serie_se_range_dans_un_cycle_mais_jamais_dans_elle_meme()
    {
        var cycle = await SerieAsync("La Légende de Drizzt");
        var trilogie = await SerieAsync("L'Elfe noir");

        var range = await _series.ModifierAsync(
            trilogie.Id, new EnregistrementSerie { Titre = trilogie.Titre, SerieParenteId = cycle.Id });

        Assert.Equal(cycle.Id, range.Serie!.SerieParenteId);

        // Refermer l'arbre sur lui-même ferait boucler l'affichage à l'infini.
        var boucle = await _series.ModifierAsync(
            cycle.Id, new EnregistrementSerie { Titre = cycle.Titre, SerieParenteId = trilogie.Id });

        Assert.NotNull(boucle.Erreur);
    }

    [Fact]
    public async Task Lordre_des_sous_series_se_reordonne_sans_toucher_aux_tomes()
    {
        var cycle = await SerieAsync("Cycle");
        var une = await SerieAsync("Une", cycle.Id);
        var deux = await SerieAsync("Deux", cycle.Id);

        var apres = (await _series.ReordonnerSousSeriesAsync(cycle.Id, [deux.Id, une.Id])).Serie!;

        Assert.Equal([deux.Id, une.Id],
            (await _series.ListerAsync()).Where(s => s.SerieParenteId == cycle.Id)
                .OrderBy(s => s.Position).Select(s => s.Id));
        Assert.Empty(apres.Elements);
    }

    [Fact]
    public async Task Supprimer_un_cycle_ne_detruit_pas_les_series_quil_contient()
    {
        var cycle = await SerieAsync("La Légende de Drizzt");
        var trilogie = await SerieAsync("L'Elfe noir", cycle.Id);

        await _series.SupprimerAsync(cycle.Id);

        var restantes = await _series.ListerAsync();
        var seule = Assert.Single(restantes);
        Assert.Equal(trilogie.Id, seule.Id);
        Assert.Null(seule.SerieParenteId);
    }

    [Fact]
    public async Task Deux_series_ne_peuvent_pas_porter_le_meme_titre()
    {
        await SerieAsync("L'Elfe noir");

        var doublon = await _series.CreerAsync(
            new EnregistrementSerie { Titre = "l'elfe  noir" }, Mathieu);

        Assert.NotNull(doublon.Erreur);
    }

    [Fact]
    public async Task Un_tome_manquant_part_dans_la_liste_denvies_de_lappelant()
    {
        var serie = await SerieAsync("L'Elfe noir");
        var apres = (await _series.AjouterElementAsync(
            serie.Id, new AjoutElementSerie { Titre = "Terre d'exil" })).Serie!;

        var resultat = await _series.MettreEnEnviesAsync(apres.Elements[0].Id, Camille);

        Assert.Null(resultat.Erreur);

        // ⚠️ La série est COMMUNE, l'envie est PERSONNELLE : elle n'apparaît que chez Camille.
        Assert.Equal(["Terre d'exil"], (await _souhaits.ListerAsync(Camille)).Select(s => s.Titre));
        Assert.Empty(await _souhaits.ListerAsync(Mathieu));
    }

    [Fact]
    public async Task Un_tome_deja_possede_ne_part_pas_en_envies()
    {
        var serie = await SerieAsync("L'Elfe noir");
        var livreId = await LivreAsync("Terre natale");
        var apres = (await _series.AjouterElementAsync(
            serie.Id, new AjoutElementSerie { LivreId = livreId })).Serie!;

        var resultat = await _series.MettreEnEnviesAsync(apres.Elements[0].Id, Mathieu);

        Assert.NotNull(resultat.Erreur);
        Assert.Empty(await _souhaits.ListerAsync(Mathieu));
    }

    [Fact]
    public async Task Detacher_un_livre_laisse_la_place_avec_son_titre()
    {
        var serie = await SerieAsync("L'Elfe noir");
        var livreId = await LivreAsync("Terre natale");
        var apres = (await _series.AjouterElementAsync(
            serie.Id, new AjoutElementSerie { LivreId = livreId, Titre = "Tome 1" })).Serie!;

        var detache = (await _series.RattacherLivreAsync(apres.Elements[0].Id, null)).Serie!;

        Assert.False(detache.Elements[0].Possede);
        Assert.Equal("Tome 1", detache.Elements[0].Titre);

        // Le livre, lui, n'a pas bougé du catalogue.
        Assert.NotNull(await _catalogue.ObtenirAsync(livreId, Mathieu));
    }

    [Fact]
    public async Task Un_tome_prete_le_dit_dans_la_serie()
    {
        var serie = await SerieAsync("L'Elfe noir");
        var livreId = await LivreAsync("Terre natale");
        await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { LivreId = livreId });

        var prets = new ServicePrets(_db);
        await prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" });

        var element = (await _series.ListerAsync()).Single().Elements.Single();

        // « Où est le tome 1 ? » a une seule réponse, la même pour tout le foyer.
        Assert.Equal("Paul", element.PreteA);
    }

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