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>
/// Table des auteurs : regroupement automatique, suggestions et fusions.
/// </summary>
public class ServiceAuteursTests : IDisposable
{
    private readonly SqliteConnection _connexion;
    private readonly MaBibliDbContext _db;
    private readonly ServiceAuteurs _auteurs;
    private readonly ServiceCatalogue _catalogue;

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

        _db = new MaBibliDbContext(
            new DbContextOptionsBuilder<MaBibliDbContext>().UseSqlite(_connexion).Options);
        _db.Database.EnsureCreated();

        _auteurs = new ServiceAuteurs(_db);
        _catalogue = new ServiceCatalogue(_db, _auteurs);
    }

    private async Task<LivreDto> AjouterAsync(string titre, string auteurs) =>
        (await _catalogue.CreerAsync(
            new EnregistrementLivre { Titre = titre, Auteur = auteurs }, "mathieu")).Livre!;

    [Fact]
    public async Task Un_auteur_nest_cree_quune_fois()
    {
        await AjouterAsync("Germinal", "Émile Zola");
        await AjouterAsync("Nana", "Émile Zola");

        var liste = await _auteurs.ListerAsync();
        var zola = Assert.Single(liste);
        Assert.Equal("Émile Zola", zola.Nom);
        Assert.Equal(2, zola.NombreDeLivres);
    }

    [Fact]
    public async Task Le_nom_daffichage_reste_celui_de_la_premiere_saisie()
    {
        await AjouterAsync("Germinal", "Émile Zola");
        await AjouterAsync("Nana", "ZOLA, emile");

        // Les deux livres pointent la même fiche, sous le nom d'origine : l'accentuation correcte
        // ne doit pas être écrasée par une saisie plus négligée.
        Assert.Equal("Émile Zola", Assert.Single(await _auteurs.ListerAsync()).Nom);
    }

    [Fact]
    public async Task Aucune_suggestion_sur_des_auteurs_sans_rapport()
    {
        await AjouterAsync("Germinal", "Émile Zola");
        await AjouterAsync("Le Horla", "Guy de Maupassant");

        Assert.Empty(await _auteurs.SuggestionsAsync());
    }

    [Fact]
    public async Task Un_patronyme_seul_est_propose_vers_le_nom_complet()
    {
        await AjouterAsync("Pandora's Star", "Peter F. Hamilton");
        await AjouterAsync("Un autre", "Hamilton");

        var suggestion = Assert.Single(await _auteurs.SuggestionsAsync());

        // La fusion efface la source : c'est le nom le moins complet qui doit disparaître.
        Assert.Equal("Hamilton", suggestion.Source.Nom);
        Assert.Equal("Peter F. Hamilton", suggestion.Cible.Nom);
    }

    [Fact]
    public async Task Accepter_un_rapprochement_reunit_les_livres()
    {
        var complet = await AjouterAsync("Pandora's Star", "Peter F. Hamilton");
        var court = await AjouterAsync("Judas Unchained", "Hamilton");

        var suggestion = Assert.Single(await _auteurs.SuggestionsAsync());
        Assert.True(await _auteurs.FusionnerAsync(suggestion.Source.Id, suggestion.Cible.Id));

        var restant = Assert.Single(await _auteurs.ListerAsync());
        Assert.Equal("Peter F. Hamilton", restant.Nom);
        Assert.Equal(2, restant.NombreDeLivres);

        // Les deux livres remontent maintenant sous le même auteur.
        var groupe = await _catalogue.ListerAsync(new CritereLivres { AuteurId = restant.Id }, "mathieu");
        Assert.Equal([complet.Id, court.Id], groupe.Select(l => l.Id).Order());

        // Et la suggestion ne peut plus revenir : l'une des deux fiches n'existe plus.
        Assert.Empty(await _auteurs.SuggestionsAsync());
    }

    [Fact]
    public async Task Refuser_un_rapprochement_le_fait_taire_definitivement()
    {
        await AjouterAsync("Pandora's Star", "Peter F. Hamilton");
        await AjouterAsync("Un autre", "Hamilton");

        var suggestion = Assert.Single(await _auteurs.SuggestionsAsync());
        Assert.True(await _auteurs.RefuserAsync(suggestion.Source.Id, suggestion.Cible.Id));

        Assert.Empty(await _auteurs.SuggestionsAsync());

        // Les deux auteurs restent bien distincts : refuser n'est pas fusionner.
        Assert.Equal(2, (await _auteurs.ListerAsync()).Count);
    }

    [Fact]
    public async Task Un_refus_vaut_dans_les_deux_sens()
    {
        await AjouterAsync("Pandora's Star", "Peter F. Hamilton");
        await AjouterAsync("Un autre", "Hamilton");

        var suggestion = Assert.Single(await _auteurs.SuggestionsAsync());

        // Refus enregistré dans l'ordre inverse de celui proposé : il doit compter quand même.
        Assert.True(await _auteurs.RefuserAsync(suggestion.Cible.Id, suggestion.Source.Id));
        Assert.Empty(await _auteurs.SuggestionsAsync());

        // Idempotent : redire non ne doit pas empiler les lignes.
        Assert.True(await _auteurs.RefuserAsync(suggestion.Source.Id, suggestion.Cible.Id));
        Assert.Equal(1, await _db.RapprochementsRefuses.CountAsync());
    }

    [Fact]
    public async Task Fusionner_un_livre_cosigne_ne_cree_pas_de_lien_en_double()
    {
        // Le même livre signé par les deux variantes : après fusion il ne doit rester qu'un lien.
        var livre = await AjouterAsync("Œuvre commune", "Peter F. Hamilton; Hamilton");
        Assert.Equal(2, livre.Auteurs.Count);

        var suggestion = Assert.Single(await _auteurs.SuggestionsAsync());
        Assert.True(await _auteurs.FusionnerAsync(suggestion.Source.Id, suggestion.Cible.Id));

        var relu = await _catalogue.ObtenirAsync(livre.Id, "mathieu");
        Assert.Single(relu!.Auteurs);
        Assert.Equal(1, await _db.LivreAuteurs.CountAsync());
    }

    [Fact]
    public async Task Fusionner_avec_un_auteur_inexistant_echoue_sans_rien_casser()
    {
        var livre = await AjouterAsync("Germinal", "Émile Zola");
        var zola = livre.Auteurs.Single().Id;

        Assert.False(await _auteurs.FusionnerAsync(zola, 4242));
        Assert.False(await _auteurs.FusionnerAsync(zola, zola));
        Assert.Equal(1, await _db.Auteurs.CountAsync());
    }

    [Fact]
    public async Task Retirer_un_auteur_dun_livre_supprime_sa_fiche_devenue_vide()
    {
        var livre = await AjouterAsync("Germinal", "Émile Zola");

        await _catalogue.ModifierAsync(
            livre.Id,
            new EnregistrementLivre { Titre = "Germinal", Auteur = "Guy de Maupassant" },
            "mathieu");

        var restant = Assert.Single(await _auteurs.ListerAsync());
        Assert.Equal("Guy de Maupassant", restant.Nom);
    }

    [Fact]
    public async Task Une_saisie_sans_lettre_ne_cree_pas_dauteur()
    {
        await AjouterAsync("Sans auteur", " ; ; .,; ");

        Assert.Empty(await _auteurs.ListerAsync());
    }

    [Fact]
    public async Task Le_meme_auteur_saisi_deux_fois_ne_donne_quun_lien()
    {
        var livre = await AjouterAsync("Germinal", "Émile Zola; ZOLA, Emile");

        Assert.Single(livre.Auteurs);
    }

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