using System.Net;
using MaBibli.Api.Data;
using MaBibli.Api.Services.Couvertures;
using MaBibli.Shared.Entites;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

namespace MaBibli.Tests;

/// <summary>
/// Relais de couvertures : ce qui est testé ici, ce n'est pas qu'une image arrive, c'est
/// <b>tout ce que le proxy refuse d'aller chercher</b>.
/// </summary>
/// <remarks>
/// ⚠️ Un proxy dont le bornage se relâche ne « marche moins bien » pas : il devient un moyen de
/// faire lire au serveur ce que l'appelant ne peut pas atteindre lui-même. Le service écoute sur
/// <c>127.0.0.1</c> et cohabite avec les autres applications YunoHost du serveur — c'est
/// exactement ce que la contrainte d'écoute cherche à empêcher.
/// </remarks>
public class ServiceCouverturesTests : IDisposable
{
    private readonly SqliteConnection _connexion;
    private readonly MaBibliDbContext _db;

    private const string UrlConnue = "https://editions-exemple.fr/couverture.jpg";

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

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

        var livre = new Livre { Titre = "Un livre", CoverUrl = UrlConnue };
        livre.RecalculerFormes();
        _db.Livres.Add(livre);
        _db.SaveChanges();
    }

    /// <summary>
    /// Client qui échoue à tout coup : ces tests portent sur le <b>bornage</b>, et un refus doit
    /// tomber <i>avant</i> tout appel réseau. Si l'un d'eux passait jusqu'ici, il échouerait —
    /// c'est précisément ce qu'on veut constater.
    /// </summary>
    private ServiceCouvertures Service() =>
        new(new HttpClient(new HandlerQuiRefuse()), _db);

    private sealed class HandlerQuiRefuse : HttpMessageHandler
    {
        public bool AEteAppele { get; private set; }

        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            AEteAppele = true;
            throw new HttpRequestException("Aucun appel réseau ne devrait avoir lieu.");
        }
    }

    // ── Verrou nº1 : l'URL doit déjà être en base ────────────────────────────

    [Fact]
    public async Task Une_url_inconnue_de_la_base_est_refusee()
    {
        var handler = new HandlerQuiRefuse();
        var service = new ServiceCouvertures(new HttpClient(handler), _db);

        var resultat = await service.ObtenirAsync("https://ailleurs.example/image.jpg", default);

        Assert.False(resultat.Trouvee);

        // Le point important : le refus tombe AVANT la requête. Sans ce verrou, le proxy irait
        // chercher n'importe quelle URL passée en paramètre.
        Assert.False(handler.AEteAppele);
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("pas une url")]
    [InlineData("file:///etc/passwd")]
    [InlineData("http://editions-exemple.fr/couverture.jpg")] // https uniquement
    public async Task Les_schemas_et_les_saisies_invalides_sont_refuses(string? url)
    {
        Assert.False((await Service().ObtenirAsync(url, default)).Trouvee);
    }

    [Fact]
    public async Task Une_url_connue_franchit_le_bornage_et_va_jusqu_au_reseau()
    {
        var handler = new HandlerQuiRefuse();
        var service = new ServiceCouvertures(new HttpClient(handler), _db);

        // Le réseau échoue (handler), donc pas de couverture — mais l'appel a bien eu lieu :
        // c'est ce qui distingue « refusé par le bornage » de « injoignable ».
        var resultat = await service.ObtenirAsync(UrlConnue, default);

        Assert.False(resultat.Trouvee);
        Assert.True(handler.AEteAppele);
    }

    [Fact]
    public async Task Une_couverture_d_ENVIE_autorise_aussi_le_relais()
    {
        // Les deux tables comptent : une envie porte sa couverture comme un livre, et elle
        // s'affiche dans la liste d'envies — donc elle doit pouvoir se mettre en cache.
        const string urlEnvie = "https://editions-exemple.fr/envie.jpg";

        var souhait = new LivreSouhaite
        {
            Utilisateur = "mathieu",
            Titre = "Une envie",
            Auteur = "Quelqu'un",
            CoverUrl = urlEnvie,
        };
        souhait.RecalculerFormes();
        _db.LivresSouhaites.Add(souhait);
        await _db.SaveChangesAsync();

        var handler = new HandlerQuiRefuse();
        var service = new ServiceCouvertures(new HttpClient(handler), _db);

        await service.ObtenirAsync(urlEnvie, default);

        Assert.True(handler.AEteAppele);
    }

    /// <summary>
    /// ⚠️ Le cas qu'on oublie : un numéro de revue porte lui aussi une couverture depuis le lot
    /// O1. L'omettre du bornage n'aurait produit aucune erreur visible — seulement une image
    /// qui s'affiche en ligne et jamais hors-ligne, la mise en cache étant refusée par le
    /// relais. C'est exactement le défaut corrigé le 2026-08-20 pour les hébergeurs sans CORS.
    /// </summary>
    [Fact]
    public async Task Une_couverture_de_NUMERO_de_revue_autorise_aussi_le_relais()
    {
        const string urlNumero = "https://editions-exemple.fr/medor-43.jpg";

        var revue = new Revue { Titre = "Médor", Issn = "2466-6718" };
        revue.RecalculerFormes();

        var numero = new NumeroRevue { Revue = revue, Numero = "43", CoverUrl = urlNumero };
        numero.RecalculerFormes();

        _db.NumerosRevue.Add(numero);
        await _db.SaveChangesAsync();

        var handler = new HandlerQuiRefuse();
        var service = new ServiceCouvertures(new HttpClient(handler), _db);

        await service.ObtenirAsync(urlNumero, default);

        Assert.True(handler.AEteAppele);
    }

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

/// <summary>
/// Verrou nº2 : la liste des adresses vers lesquelles une connexion peut s'ouvrir.
/// </summary>
/// <remarks>
/// C'est le verrou qui tient <b>même si</b> une URL interne a été enregistrée en base dans une
/// fiche : la valeur serait connue, la connexion serait refusée quand même.
/// </remarks>
public class GardeAdressesTests
{
    [Theory]
    [InlineData("127.0.0.1")]          // bouclage : le service lui-même
    [InlineData("127.1.2.3")]
    [InlineData("0.0.0.0")]
    [InlineData("10.0.0.5")]           // privé
    [InlineData("172.16.0.1")]
    [InlineData("172.31.255.254")]
    [InlineData("192.168.1.1")]
    [InlineData("169.254.169.254")]    // lien-local : le point de métadonnées des hébergeurs
    [InlineData("100.64.0.1")]         // CGNAT
    [InlineData("224.0.0.1")]          // multicast
    [InlineData("::1")]                // bouclage IPv6
    [InlineData("::")]
    [InlineData("fe80::1")]            // lien-local IPv6
    [InlineData("fc00::1")]            // adresses locales uniques
    [InlineData("fd12:3456::1")]
    [InlineData("::ffff:127.0.0.1")]   // bouclage déguisé en IPv6
    [InlineData("::ffff:192.168.0.1")]
    public void Les_adresses_internes_sont_refusees(string adresse)
    {
        Assert.False(GardeAdresses.EstPubliquementRoutable(IPAddress.Parse(adresse)));
    }

    [Theory]
    [InlineData("1.1.1.1")]
    [InlineData("172.15.0.1")]         // juste HORS de 172.16.0.0/12
    [InlineData("172.32.0.1")]         // juste au-dessus
    [InlineData("100.63.255.255")]     // juste sous le CGNAT
    [InlineData("100.128.0.1")]        // juste au-dessus
    [InlineData("192.167.0.1")]
    [InlineData("2606:4700:4700::1111")]
    public void Les_adresses_publiques_sont_acceptees(string adresse)
    {
        Assert.True(GardeAdresses.EstPubliquementRoutable(IPAddress.Parse(adresse)));
    }
}
