using MaBibli.Api.Data;
using MaBibli.Shared.Entites;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;

namespace MaBibli.Tests;

/// <summary>
/// Rattrapage des colonnes normalisées après une migration.
/// </summary>
/// <remarks>
/// Les données sont écrites <b>telles que la migration SQL les laisse</b> : normalisation
/// approchée (<c>lower()</c> ne touche pas aux accents), donc variantes d'un même auteur encore
/// éclatées. C'est exactement l'état que ce service doit savoir rattraper.
/// </remarks>
public class ServiceRenormalisationTests : IDisposable
{
    private readonly SqliteConnection _connexion;
    private readonly MaBibliDbContext _db;

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

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

    private Task ExecuterAsync() =>
        new ServiceRenormalisation(_db, NullLogger<ServiceRenormalisation>.Instance).ExecuterAsync();

    /// <summary>Écrit un auteur comme la migration le ferait : formes en <c>lower()</c> brut.</summary>
    private Auteur AjouterHerite(string nom, params string[] titres)
    {
        var auteur = new Auteur
        {
            Nom = nom,
            NomNormalise = nom.ToLowerInvariant(),
            CleRegroupement = nom.ToLowerInvariant(),
        };

        _db.Auteurs.Add(auteur);

        foreach (var titre in titres)
        {
            var livre = new Livre { Titre = titre, TitreNormalise = titre.ToLowerInvariant() };
            _db.Livres.Add(livre);
            livre.Auteurs.Add(new LivreAuteur { Auteur = auteur });
        }

        _db.SaveChanges();
        return auteur;
    }

    [Fact]
    public async Task Les_titres_perdent_leurs_accents_dans_la_colonne_normalisee()
    {
        AjouterHerite("Émile Zola", "La Bête humaine");

        await ExecuterAsync();

        Assert.Equal("la bete humaine", _db.Livres.Single().TitreNormalise);
    }

    [Fact]
    public async Task Les_variantes_daccent_et_dordre_sont_reunies()
    {
        AjouterHerite("Emile Zola", "Germinal");
        AjouterHerite("Émile Zola", "Nana");
        AjouterHerite("ZOLA, Émile", "La Curée");

        await ExecuterAsync();

        var zola = Assert.Single(_db.Auteurs);
        Assert.Equal(3, await _db.LivreAuteurs.CountAsync(la => la.AuteurId == zola.Id));
    }

    /// <summary>
    /// Le nom qui survit est celui qui se lit le mieux. Sans arbitrage, c'est l'ordre — arbitraire —
    /// des identifiants issus de la migration qui déciderait, et la bibliothèque perdrait ses
    /// accents au profit de la saisie la plus négligée.
    /// </summary>
    [Theory]
    // L'ordre est celui des identifiants après migration : le résultat doit en être indépendant.
    [InlineData("Emile Zola|Émile Zola")]
    [InlineData("Émile Zola|Emile Zola")]
    [InlineData("EMILE ZOLA|ZOLA, Émile|Émile Zola")]
    [InlineData("ZOLA, Émile|Émile Zola")]
    public async Task Le_nom_conserve_est_le_plus_presentable(string variantes)
    {
        foreach (var variante in variantes.Split('|'))
        {
            AjouterHerite(variante, $"Livre de {variante}");
        }

        await ExecuterAsync();

        Assert.Equal("Émile Zola", Assert.Single(_db.Auteurs).Nom);
    }

    /// <summary>
    /// Les initiales échappent à la clé de regroupement. Sans second tour, une base héritée
    /// resterait éclatée là où une saisie neuve aurait été réunie d'emblée.
    /// </summary>
    [Fact]
    public async Task Les_initiales_abregees_sont_reunies_sous_le_nom_le_plus_complet()
    {
        AjouterHerite("P.F. Hamilton", "Pandora's Star");
        AjouterHerite("Peter F. Hamilton", "Judas Unchained");

        await ExecuterAsync();

        var hamilton = Assert.Single(_db.Auteurs);
        Assert.Equal("Peter F. Hamilton", hamilton.Nom);
        Assert.Equal(2, await _db.LivreAuteurs.CountAsync());
    }

    /// <summary>Le cas ambigu reste ambigu : il sera proposé, pas appliqué.</summary>
    [Fact]
    public async Task Un_patronyme_seul_nest_jamais_absorbe()
    {
        AjouterHerite("Hamilton", "Great North Road");
        AjouterHerite("Peter F. Hamilton", "Pandora's Star");

        await ExecuterAsync();

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

    [Fact]
    public async Task Un_livre_cosigne_par_deux_variantes_ne_garde_quun_lien()
    {
        var livre = new Livre { Titre = "Œuvre commune", TitreNormalise = "œuvre commune" };
        _db.Livres.Add(livre);
        _db.SaveChanges();

        foreach (var nom in new[] { "Emile Zola", "Émile Zola" })
        {
            var auteur = new Auteur
            {
                Nom = nom,
                NomNormalise = nom.ToLowerInvariant(),
                CleRegroupement = nom.ToLowerInvariant(),
            };

            _db.Auteurs.Add(auteur);
            _db.LivreAuteurs.Add(new LivreAuteur { LivreId = livre.Id, Auteur = auteur });
        }

        _db.SaveChanges();

        await ExecuterAsync();

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

    [Fact]
    public async Task Un_second_passage_ne_change_plus_rien()
    {
        AjouterHerite("Emile Zola", "Germinal");
        AjouterHerite("Émile Zola", "Nana");

        await ExecuterAsync();
        var apresPremier = _db.Auteurs.Single();
        var nom = apresPremier.Nom;
        var cle = apresPremier.CleRegroupement;

        await ExecuterAsync();

        var apresSecond = Assert.Single(_db.Auteurs);
        Assert.Equal(nom, apresSecond.Nom);
        Assert.Equal(cle, apresSecond.CleRegroupement);
        Assert.Equal(2, await _db.LivreAuteurs.CountAsync());
    }

    [Fact]
    public async Task Une_base_vide_ne_pose_aucun_probleme()
    {
        await ExecuterAsync();

        Assert.Empty(_db.Auteurs);
        Assert.Empty(_db.Livres);
    }

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