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

namespace MaBibli.Tests;

/// <summary>
/// Revues et numéros, sur une base SQLite <b>en mémoire</b> : l'unicité d'un ISSN passe par un
/// index partiel, qui n'existe qu'en base.
/// </summary>
/// <remarks>
/// L'invariant central : <b>une fiche par revue</b>, les numéros à l'intérieur. C'est ce qui
/// évite douze fiches identiques pour douze numéros d'un même magazine.
/// </remarks>
public class ServiceRevuesTests : IDisposable
{
    private readonly SqliteConnection _connexion;
    private readonly MaBibliDbContext _db;
    private readonly ServiceRevues _revues;

    private const string Mathieu = "mathieu";

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

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

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

        _revues = new ServiceRevues(_db);
    }

    private async Task<RevueDto> RevueAsync(string titre = "Médor", string? issn = "2466-6718")
    {
        var resultat = await _revues.CreerOuRetrouverAsync(
            new EnregistrementRevue { Titre = titre, Issn = issn }, Mathieu);

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

    [Fact]
    public async Task Rescanner_la_meme_revue_ne_cree_pas_une_seconde_fiche()
    {
        var premiere = await RevueAsync();

        // Le second numéro du même magazine porte le même ISSN : c'est le cas NORMAL du flux
        // « 977 », et il ne doit surtout pas produire une fiche de plus.
        var seconde = await RevueAsync("Médor (Bruxelles)");

        Assert.Equal(premiere.Id, seconde.Id);
        Assert.Single(await _revues.ListerAsync());
    }

    [Fact]
    public async Task Une_revue_saisie_a_la_main_apprend_son_issn_au_premier_scan()
    {
        var aLaMain = await RevueAsync("Médor", issn: null);

        var scannee = await RevueAsync("Médor");

        Assert.Equal(aLaMain.Id, scannee.Id);
        Assert.Equal("2466-6718", scannee.Issn);
    }

    [Fact]
    public async Task Les_numeros_vivent_dans_la_revue_et_ne_font_pas_de_fiches()
    {
        var revue = await RevueAsync();

        await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue
        {
            Numero = "43",
            DateParution = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc),
        });

        var apres = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue
        {
            Numero = "44",
            DateParution = new DateTime(2024, 9, 1, 0, 0, 0, DateTimeKind.Utc),
        })).Revue!;

        Assert.Single(await _revues.ListerAsync());

        // Du plus récent au plus ancien : sur un magazine, c'est le dernier reçu qu'on cherche.
        Assert.Equal(["44", "43"], apres.Numeros.Select(n => n.Numero));
    }

    [Fact]
    public async Task Un_numero_sans_numero_est_refuse()
    {
        var revue = await RevueAsync();

        var resultat = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "  " });

        Assert.NotNull(resultat.Erreur);
    }

    [Fact]
    public async Task Le_meme_numero_ne_sajoute_pas_deux_fois()
    {
        var revue = await RevueAsync();
        await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43" });

        var doublon = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43" });

        Assert.NotNull(doublon.Erreur);
    }

    [Fact]
    public async Task Un_numero_a_paraitre_est_refuse()
    {
        var revue = await RevueAsync();

        var resultat = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue
        {
            Numero = "99",
            DateParution = DateTime.UtcNow.AddMonths(2),
        });

        Assert.NotNull(resultat.Erreur);
    }

    [Fact]
    public async Task Les_numeros_sans_date_ferment_la_liste()
    {
        var revue = await RevueAsync();
        await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "hors-série 1" });

        var apres = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue
        {
            Numero = "43",
            DateParution = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc),
        })).Revue!;

        // Une date inconnue ne doit pas faire passer un numéro pour le plus ancien.
        Assert.Equal(["43", "hors-série 1"], apres.Numeros.Select(n => n.Numero));
    }

    [Fact]
    public async Task Supprimer_la_revue_emporte_ses_numeros()
    {
        var revue = await RevueAsync();
        await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43" });

        Assert.True(await _revues.SupprimerAsync(revue.Id));

        // Contrairement aux livres d'une série, un numéro n'existe que par sa revue.
        Assert.Empty(await _db.NumerosRevue.ToListAsync());
    }

    [Fact]
    public async Task Deux_revues_ne_partagent_ni_titre_ni_issn()
    {
        await RevueAsync();
        var autre = await RevueAsync("Socialter", issn: "2270-4426");

        var titrePris = await _revues.ModifierAsync(
            autre.Id, new EnregistrementRevue { Titre = "médor" });
        var issnPris = await _revues.ModifierAsync(
            autre.Id, new EnregistrementRevue { Titre = "Socialter", Issn = "2466-6718" });

        Assert.NotNull(titrePris.Erreur);
        Assert.NotNull(issnPris.Erreur);
    }

    /// <summary>
    /// ⚠️ Le cas qui a motivé la canonisation (lot K2) : une revue saisie à la main, ISSN tapé
    /// sans tiret, doit être <b>retrouvée</b> par le scan — qui produit toujours la forme à
    /// tiret. Sans elle, le code-barres créait une seconde fiche à côté de la première.
    /// </summary>
    [Fact]
    public async Task Un_issn_sans_tiret_designe_la_meme_revue_que_le_code_barres()
    {
        var aLaMain = await RevueAsync("Médor", issn: "24666718");

        Assert.Equal("2466-6718", aLaMain.Issn);

        var scanne = await RevueAsync("Médor (Bruxelles)", issn: "2466-6718");

        Assert.Equal(aLaMain.Id, scanne.Id);
        Assert.Single(await _revues.ListerAsync());
    }

    [Fact]
    public async Task Un_numero_porte_sa_couverture_et_ses_articles_a_la_une()
    {
        var revue = await RevueAsync();

        var apres = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue
        {
            Numero = "43",
            CoverUrl = "https://exemple.fr/medor-43.jpg",
            Articles = ["Ukraine, deux ans après", "Le retour du train de nuit"],
        });

        var numero = Assert.Single(apres.Revue!.Numeros);

        Assert.Equal("https://exemple.fr/medor-43.jpg", numero.CoverUrl);
        Assert.Equal(["Ukraine, deux ans après", "Le retour du train de nuit"], numero.Articles);
    }

    /// <summary>
    /// La une se recopie <b>après coup</b> : on note un numéro le jour où on le range, on en
    /// recopie le sommaire plus tard. Sans modification, ces champs n'auraient existé qu'à la
    /// création, c'est-à-dire jamais.
    /// </summary>
    [Fact]
    public async Task Un_numero_apprend_sa_une_apres_coup()
    {
        var revue = await RevueAsync();
        var cree = (await _revues.AjouterNumeroAsync(
            revue.Id, new AjoutNumeroRevue { Numero = "43" })).Revue!;

        var numeroId = cree.Numeros[0].Id;

        var apres = await _revues.ModifierNumeroAsync(numeroId, new AjoutNumeroRevue
        {
            Numero = "43",
            Articles = ["Le retour du train de nuit"],
        });

        Assert.Equal(["Le retour du train de nuit"], apres.Revue!.Numeros[0].Articles);
    }

    /// <summary>
    /// ⚠️ Remplacement et non fusion : la ligne de saisie <b>est</b> la liste. Un titre effacé
    /// du champ doit disparaître, comme pour les thèmes d'un livre.
    /// </summary>
    [Fact]
    public async Task Reecrire_la_une_remplace_lancienne()
    {
        var revue = await RevueAsync();
        var cree = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue
        {
            Numero = "43",
            Articles = ["Premier", "Second"],
        })).Revue!;

        var apres = await _revues.ModifierNumeroAsync(
            cree.Numeros[0].Id, new AjoutNumeroRevue { Numero = "43", Articles = ["Troisième"] });

        Assert.Equal(["Troisième"], apres.Revue!.Numeros[0].Articles);
        Assert.Single(await _db.ArticlesUne.ToListAsync());
    }

    [Fact]
    public async Task Retirer_le_numero_emporte_sa_une()
    {
        var revue = await RevueAsync();
        var cree = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue
        {
            Numero = "43",
            Articles = ["Ukraine, deux ans après"],
        })).Revue!;

        Assert.True(await _revues.RetirerNumeroAsync(cree.Numeros[0].Id));

        // Cascade : un article à la une n'existe que par la parution qui le porte.
        Assert.Empty(await _db.ArticlesUne.ToListAsync());
    }

    /// <summary>
    /// L'unicité <c>(NumeroRevueId, TitreNormalise)</c> vit en base : un doublon envoyé
    /// directement à l'API ferait échouer l'écriture si le service ne l'écartait pas.
    /// </summary>
    [Fact]
    public async Task Le_meme_article_ne_sannonce_pas_deux_fois()
    {
        var revue = await RevueAsync();

        var apres = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue
        {
            Numero = "43",
            Articles = ["Ukraine, deux ans après", "ukraine, DEUX ans après", "  "],
        });

        Assert.Equal(["Ukraine, deux ans après"], apres.Revue!.Numeros[0].Articles);
    }

    [Fact]
    public async Task Deux_numeros_peuvent_titrer_pareil()
    {
        var revue = await RevueAsync();

        await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue
        {
            Numero = "43",
            Articles = ["Le dossier de l'année"],
        });

        var apres = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue
        {
            Numero = "44",
            Articles = ["Le dossier de l'année"],
        });

        // L'unicité est bornée au numéro : deux parutions peuvent parfaitement titrer pareil.
        Assert.Null(apres.Erreur);
        Assert.Equal(2, (await _db.ArticlesUne.ToListAsync()).Count);
    }

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