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

namespace MaBibli.Tests;

/// <summary>
/// Liste d'envies, sur une base SQLite <b>en mémoire</b> : l'unicité « une œuvre par personne »
/// n'existe qu'en base, elle ne se vérifierait pas sur un faux dépôt.
/// </summary>
/// <remarks>
/// L'invariant central testé ici est la <b>portée personnelle</b> : deux utilisateurs ne doivent
/// jamais se voir. C'est l'exact contraire du catalogue, commun au foyer — et une régression y
/// serait discrète, puisque tout continuerait de « marcher » avec un seul compte.
/// </remarks>
public class ServiceSouhaitsTests : IDisposable
{
    private readonly SqliteConnection _connexion;
    private readonly MaBibliDbContext _db;
    private readonly ServiceSouhaits _souhaits;
    private readonly ServiceCatalogue _catalogue;

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

    public ServiceSouhaitsTests()
    {
        _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);
        _catalogue = new ServiceCatalogue(_db, new ServiceAuteurs(_db));
    }

    private Task<ResultatSouhait> AjouterAsync(
        string titre, string? auteur = "Bernard Werber", string utilisateur = Mathieu,
        string? isbn = null, string? note = null) =>
        _souhaits.AjouterAsync(
            new EnregistrementSouhait { Titre = titre, Auteur = auteur, Isbn = isbn, Note = note },
            utilisateur);

    // ── Portée personnelle ───────────────────────────────────────────────────

    [Fact]
    public async Task Deux_utilisateurs_ont_deux_listes_distinctes()
    {
        await AjouterAsync("Les fourmis", utilisateur: Mathieu);
        await AjouterAsync("Germinal", "Émile Zola", Camille);

        var deMathieu = await _souhaits.ListerAsync(Mathieu);
        var deCamille = await _souhaits.ListerAsync(Camille);

        Assert.Equal("Les fourmis", Assert.Single(deMathieu).Titre);
        Assert.Equal("Germinal", Assert.Single(deCamille).Titre);
    }

    [Fact]
    public async Task Deux_utilisateurs_peuvent_souhaiter_le_meme_livre()
    {
        var premier = await AjouterAsync("Les fourmis", utilisateur: Mathieu);
        var second = await AjouterAsync("Les fourmis", utilisateur: Camille);

        // L'unicité porte sur (utilisateur, œuvre) : elle ne doit pas empêcher deux personnes
        // d'avoir la même envie — c'est même le cas normal dans un foyer.
        Assert.Null(premier.Erreur);
        Assert.Null(second.Erreur);
    }

    [Fact]
    public async Task On_ne_supprime_pas_l_envie_d_un_autre()
    {
        var souhait = await AjouterAsync("Les fourmis", utilisateur: Mathieu);

        var supprime = await _souhaits.SupprimerAsync(souhait.Souhait!.Id, Camille);

        Assert.False(supprime);
        Assert.Single(await _souhaits.ListerAsync(Mathieu));
    }

    [Fact]
    public async Task Sans_identite_il_n_y_a_ni_liste_ni_ecriture()
    {
        var ajout = await AjouterAsync("Les fourmis", utilisateur: null!);

        Assert.NotNull(ajout.Erreur);
        Assert.Empty(await _souhaits.ListerAsync(null));
    }

    // ── Cloisonnement d'avec le catalogue ────────────────────────────────────

    /// <summary>
    /// L'invariant qui justifie l'entité séparée : une envie n'entre ni dans le catalogue, ni
    /// dans les compteurs, ni dans la table des auteurs.
    /// </summary>
    [Fact]
    public async Task Une_envie_n_apparait_pas_dans_le_catalogue()
    {
        await AjouterAsync("La prophétie des abeilles", "Bernard Werber");

        var catalogue = await _catalogue.ListerAsync(new CritereLivres(), Mathieu);

        Assert.Empty(catalogue);
    }

    [Fact]
    public async Task Une_envie_ne_cree_pas_de_fiche_auteur()
    {
        await AjouterAsync("La prophétie des abeilles", "Bernard Werber");

        // Sinon « Bernard Werber » apparaîtrait sur l'écran des auteurs avec « 0 livre »,
        // et trahirait au passage la liste d'envies de son propriétaire.
        Assert.Empty(await new ServiceAuteurs(_db).ListerAsync());
    }

    [Fact]
    public async Task Une_envie_recherchee_dans_le_catalogue_ne_remonte_pas()
    {
        await _catalogue.CreerAsync(
            new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
        await AjouterAsync("La prophétie des abeilles", "Bernard Werber");

        var trouves = await _catalogue.ListerAsync(
            new CritereLivres { Recherche = "Werber" }, Mathieu);

        Assert.Equal("Les fourmis", Assert.Single(trouves).Titre);
    }

    // ── Validation et doublons ───────────────────────────────────────────────

    [Fact]
    public async Task Le_titre_est_obligatoire()
    {
        var resultat = await AjouterAsync("   ");

        Assert.NotNull(resultat.Erreur);
    }

    [Fact]
    public async Task La_meme_oeuvre_ne_s_ajoute_pas_deux_fois()
    {
        await AjouterAsync("Les thanatonautes");

        // Variante d'écriture du même titre : c'est exactement ce que produit un double clic
        // sur deux lignes de bibliographie qui n'ont pas été regroupées côté BnF.
        var doublon = await AjouterAsync("Les thanatonautes : roman");

        Assert.NotNull(doublon.Erreur);
        Assert.Single(await _souhaits.ListerAsync(Mathieu));
    }

    [Fact]
    public async Task Deux_auteurs_peuvent_avoir_une_oeuvre_de_meme_titre()
    {
        var premier = await AjouterAsync("Nouvelles", "Bernard Werber");
        var second = await AjouterAsync("Nouvelles", "Émile Zola");

        Assert.Null(premier.Erreur);
        Assert.Null(second.Erreur);
        Assert.Equal(2, (await _souhaits.ListerAsync(Mathieu)).Count);
    }

    [Fact]
    public async Task Un_ISBN_invalide_est_refuse()
    {
        var resultat = await AjouterAsync("Les fourmis", isbn: "1234567890123");

        Assert.NotNull(resultat.Erreur);
    }

    [Fact]
    public async Task L_ISBN_reste_facultatif()
    {
        var resultat = await AjouterAsync("Les fourmis", isbn: null);

        Assert.Null(resultat.Erreur);
        Assert.Null(resultat.Souhait!.Isbn);
    }

    [Fact]
    public async Task La_liste_est_ordonnee_par_auteur_puis_titre()
    {
        await AjouterAsync("Les fourmis", "Bernard Werber");
        await AjouterAsync("Sans auteur", null);
        await AjouterAsync("Germinal", "Émile Zola");
        await AjouterAsync("Demain les chats", "Bernard Werber");

        var titres = (await _souhaits.ListerAsync(Mathieu)).Select(s => s.Titre).ToList();

        // Werber avant Zola, ses titres classés entre eux, et l'envie sans auteur en dernier.
        Assert.Equal(["Demain les chats", "Les fourmis", "Germinal", "Sans auteur"], titres);
    }

    [Fact]
    public async Task Une_envie_supprimee_disparait()
    {
        var souhait = await AjouterAsync("Les fourmis");

        Assert.True(await _souhaits.SupprimerAsync(souhait.Souhait!.Id, Mathieu));
        Assert.Empty(await _souhaits.ListerAsync(Mathieu));
    }

    // ── Bibliographie : confrontation à l'étagère ────────────────────────────

    /// <summary>
    /// Client BnF simulé : ces tests portent sur le rapprochement, pas sur le réseau.
    /// </summary>
    private sealed class BnfBibliographieFake(string xml) : IBnfClient
    {
        public Task<(IReadOnlyList<CandidatLivre>, string?)> RechercherAsync(
            string isbn, string? urlCouverture, CancellationToken ct = default) =>
            Task.FromResult(((IReadOnlyList<CandidatLivre>)[], (string?)null));

        public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync(
            string auteur, CancellationToken ct = default) =>
            Task.FromResult((BnfBibliographieParser.Parser(xml, auteur), (string?)null));
    }

    private async Task<BibliographieDto> BibliographieWerberAsync(string utilisateur = Mathieu)
    {
        var auteurs = new ServiceAuteurs(_db);
        var service = new ServiceBibliographie(
            _db, new BnfBibliographieFake(Fixture.Lire("bnf-auteur-werber.xml")), auteurs);

        var werber = (await auteurs.ListerAsync()).Single(a => a.Nom.Contains("Werber"));
        return (await service.ObtenirAsync(werber.Id, utilisateur))!;
    }

    [Fact]
    public async Task La_bibliographie_grise_les_livres_deja_possedes()
    {
        // Titre saisi sans le sous-titre « : roman » que porte la notice BnF : c'est le cas
        // courant, et c'est précisément ce que la clé d'œuvre doit rattraper.
        await _catalogue.CreerAsync(
            new EnregistrementLivre { Titre = "Les thanatonautes", Auteur = "Bernard Werber" }, Mathieu);

        var biblio = await BibliographieWerberAsync();

        var oeuvre = biblio.Oeuvres.Single(
            o => o.Titre.StartsWith("Les thanatonautes", StringComparison.OrdinalIgnoreCase));

        Assert.True(oeuvre.Possede);
        Assert.NotNull(oeuvre.LivreId);

        // Les autres restent à découvrir : posséder un livre n'en grise pas d'autres.
        Assert.Contains(biblio.Oeuvres, o => o.ADecouvrir);
    }

    [Fact]
    public async Task La_bibliographie_signale_ce_qui_est_deja_souhaite()
    {
        await _catalogue.CreerAsync(
            new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);

        var avant = await BibliographieWerberAsync();
        var cible = avant.Oeuvres.First(o => o.ADecouvrir);

        await AjouterAsync(cible.Titre, "Bernard Werber");

        var apres = await BibliographieWerberAsync();
        var revue = apres.Oeuvres.Single(o => o.Titre == cible.Titre);

        Assert.True(revue.Souhaite);
        Assert.False(revue.ADecouvrir);
    }

    /// <summary>
    /// « Possédé » vaut pour le foyer, « souhaité » seulement pour l'appelant : c'est ce qui
    /// permet d'offrir un livre à quelqu'un sans qu'il le voie venir.
    /// </summary>
    [Fact]
    public async Task Le_souhait_d_un_autre_n_apparait_pas_dans_ma_bibliographie()
    {
        await _catalogue.CreerAsync(
            new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);

        var biblio = await BibliographieWerberAsync(Mathieu);
        var cible = biblio.Oeuvres.First(o => o.ADecouvrir);

        await AjouterAsync(cible.Titre, "Bernard Werber", Camille);

        var vueDeMathieu = await BibliographieWerberAsync(Mathieu);
        var vueDeCamille = await BibliographieWerberAsync(Camille);

        Assert.False(vueDeMathieu.Oeuvres.Single(o => o.Titre == cible.Titre).Souhaite);
        Assert.True(vueDeCamille.Oeuvres.Single(o => o.Titre == cible.Titre).Souhaite);
    }

    [Fact]
    public async Task Un_auteur_inconnu_n_a_pas_de_bibliographie()
    {
        var service = new ServiceBibliographie(
            _db, new BnfBibliographieFake("<x/>"), new ServiceAuteurs(_db));

        Assert.Null(await service.ObtenirAsync(4242, Mathieu));
    }

    /// <summary>
    /// Le livre d'un homonyme ne doit pas griser l'œuvre d'un autre : la comparaison se fait
    /// à auteur donné, pas sur tout le catalogue.
    /// </summary>
    [Fact]
    public async Task Un_livre_d_un_autre_auteur_ne_grise_rien()
    {
        await _catalogue.CreerAsync(
            new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);

        // Un homonyme parfait, mais signé de quelqu'un d'autre.
        await _catalogue.CreerAsync(
            new EnregistrementLivre { Titre = "Les thanatonautes", Auteur = "Éric Corbeyran" }, Mathieu);

        var biblio = await BibliographieWerberAsync();

        Assert.False(
            biblio.Oeuvres
                .Single(o => o.Titre.StartsWith("Les thanatonautes", StringComparison.OrdinalIgnoreCase))
                .Possede);
    }

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