using System.Text;
using Sunfish.Config;

namespace Sunfish.Tests;

/// <summary>Servidor real sobre localhost, hablando HTTP en bruto como lo haria un cliente antiguo.</summary>
public class WebServerTests : IAsyncLifetime
{
    private const string Modern = "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0";

    private readonly TempDir _web = new();
    private readonly TempDir _fotos = new();
    private TestServer _server = null!;

    public Task InitializeAsync()
    {
        _web.File("index.html", "<html>portada</html>");
        _web.File("leeme.txt", "hola");
        _web.File("docs/manual.txt", "0123456789");
        _web.File("sitio/index.html", "<html>sitio</html>");
        _web.File("fotos/tapada.txt", "no deberia verse");
        _fotos.File("año 2026.jpg", "jpeg");
        _fotos.File(".oculto", "x");

        var config = new SunfishConfig();
        config.Shares.Add(new ShareConfig { Name = "web", Location = "/", Path = _web.Path });
        config.Shares.Add(new ShareConfig { Name = "fotos", Location = "/fotos", Path = _fotos.Path });
        config.Shares.Add(new ShareConfig { Name = "privado", Location = "/privado", Path = _fotos.Path, Listing = false, Index = "" });
        config.Shares.Add(new ShareConfig { Name = "hondo", Location = "/a/b/c", Path = _fotos.Path });
        _server = new TestServer(config);
        return Task.CompletedTask;
    }

    public async Task DisposeAsync()
    {
        await _server.DisposeAsync();
        _web.Dispose();
        _fotos.Dispose();
    }

    [Fact]
    public async Task ServesIndexAtRoot()
    {
        var r = await _server.GetAsync("/");
        Assert.Equal(200, r.Status);
        Assert.Equal("<html>portada</html>", r.BodyText);
        Assert.Equal("text/html", r.Headers["Content-Type"]);
    }

    [Fact]
    public async Task ListParameterShowsTheFolderDespiteTheIndex()
    {
        var r = await _server.GetAsync("/?$list");
        Assert.Equal(200, r.Status);
        Assert.Contains("leeme.txt", r.BodyText);
        Assert.Contains("HREF=\"sitio/\"", r.BodyText);
        Assert.Contains("sitio/?$list", r.BodyText); // carpeta con indice: enlace [contenido]
    }

    [Fact]
    public async Task LongerLocationHidesTheRealFolder()
    {
        var r = await _server.GetAsync("/fotos/");
        Assert.Equal(200, r.Status);
        Assert.Contains("a%C3%B1o%202026.jpg", r.BodyText);
        Assert.DoesNotContain("tapada", r.BodyText);
        Assert.DoesNotContain(".oculto", r.BodyText);
    }

    [Fact]
    public async Task ClassicPagesAreLatin1WithEntities()
    {
        var r = await _server.GetAsync("/fotos/", "Mozilla/3.01 (Win95; I)");
        Assert.Equal("text/html; charset=iso-8859-1", r.Headers["Content-Type"]);
        Assert.Contains("a&#241;o 2026.jpg", r.BodyText);
        Assert.DoesNotContain("<script", r.BodyText, StringComparison.OrdinalIgnoreCase);
        Assert.All(r.Body, b => Assert.True(b < 0x80));
    }

    [Fact]
    public async Task ModernPagesAreUtf8()
    {
        var r = await _server.GetAsync("/fotos/", Modern);
        Assert.Equal("text/html; charset=utf-8", r.Headers["Content-Type"]);
        Assert.Contains("año 2026.jpg", Encoding.UTF8.GetString(r.Body));
        Assert.Contains("font-family:Verdana", r.BodyText);
    }

    [Fact]
    public async Task ForcedProfileIsPropagatedInLinks()
    {
        var r = await _server.GetAsync("/?$list&p=dos");
        Assert.Contains("HREF=\"docs/?p=dos\"", r.BodyText);
        Assert.Contains("<UL>", r.BodyText); // DOS: lista apta para navegadores de texto
    }

    [Fact]
    public async Task DownloadsFilesWithLatin1Url()
    {
        var r = await _server.GetAsync("/fotos/a%F1o%202026.jpg");
        Assert.Equal(200, r.Status);
        Assert.Equal("jpeg", r.BodyText);
        Assert.Equal("image/jpeg", r.Headers["Content-Type"]);
        Assert.Equal("4", r.Headers["Content-Length"]);
    }

    [Fact]
    public async Task RedirectsFoldersWithoutSlash()
    {
        var r = await _server.GetAsync("/docs", extraHeaders: "Host: equipo:8080\r\n");
        Assert.Equal(301, r.Status);
        Assert.Equal("http://equipo:8080/docs/", r.Headers["Location"]);
    }

    [Fact]
    public async Task VirtualIntermediateFolders()
    {
        var r = await _server.GetAsync("/a/b/");
        Assert.Equal(200, r.Status);
        Assert.Contains("HREF=\"c/\"", r.BodyText);
    }

    [Fact]
    public async Task ForbiddenWhenListingIsOff()
    {
        Assert.Equal(403, (await _server.GetAsync("/privado/")).Status);
        Assert.Equal(200, (await _server.GetAsync("/privado/a%C3%B1o%202026.jpg")).Status);
    }

    [Theory]
    [InlineData("/../../Windows/win.ini")]
    [InlineData("/%2e%2e/%2e%2e/etc/passwd")]
    [InlineData("/fotos/..%5c..%5csecreto")]
    public async Task TraversalIsRejected(string path)
    {
        var r = await _server.GetAsync(path);
        Assert.Equal(400, r.Status);
    }

    [Fact]
    public async Task NotFound() => Assert.Equal(404, (await _server.GetAsync("/no/existe.txt")).Status);

    [Fact]
    public async Task Http09GetsOnlyTheBody()
    {
        var r = await _server.SendAsync("GET /leeme.txt\r\n");
        Assert.Equal("hola", Encoding.ASCII.GetString(r.Data));
    }

    [Fact]
    public async Task HeadHasNoBody()
    {
        var r = await _server.SendAsync("HEAD /docs/manual.txt HTTP/1.0\r\n\r\n");
        Assert.Equal(200, r.Status);
        Assert.Equal("10", r.Headers["Content-Length"]);
        Assert.Empty(r.Body);
    }

    [Fact]
    public async Task RangeRequests()
    {
        var r = await _server.GetAsync("/docs/manual.txt", extraHeaders: "Range: bytes=3-5\r\n");
        Assert.Equal(206, r.Status);
        Assert.Equal("345", r.BodyText);
        Assert.Equal("bytes 3-5/10", r.Headers["Content-Range"]);

        r = await _server.GetAsync("/docs/manual.txt", extraHeaders: "Range: bytes=50-\r\n");
        Assert.Equal(416, r.Status);
    }

    [Fact]
    public async Task NotModified()
    {
        var first = await _server.GetAsync("/leeme.txt");
        var r = await _server.GetAsync("/leeme.txt", extraHeaders: $"If-Modified-Since: {first.Headers["Last-Modified"]}\r\n");
        Assert.Equal(304, r.Status);
        Assert.Empty(r.Body);
    }

    [Fact]
    public async Task KeepAliveServesSeveralRequests()
    {
        var r = await _server.SendAsync(
            "GET /leeme.txt HTTP/1.1\r\nHost: x\r\n\r\n" +
            "GET /docs/manual.txt HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n");
        string text = r.BodyText;
        Assert.Contains("hola", text);
        Assert.EndsWith("0123456789", Encoding.ASCII.GetString(r.Data));
    }

    [Fact]
    public async Task ServesEmbeddedResources()
    {
        var r = await _server.GetAsync("/$sunfish/res/logo.gif");
        Assert.Equal(200, r.Status);
        Assert.Equal("image/gif", r.Headers["Content-Type"]);
        Assert.StartsWith("GIF8", r.BodyText);
        Assert.Equal(200, (await _server.GetAsync("/favicon.ico")).Status);
    }

    [Fact]
    public async Task UnknownMethodsAndDeleteWithoutPermission()
    {
        var r = await _server.SendAsync("DELETE /leeme.txt HTTP/1.0\r\n\r\n");
        Assert.Equal(403, r.Status); // WebDAV DELETE sin permiso de borrar
        Assert.True(File.Exists(Path.Combine(_web.Path, "leeme.txt")));
        Assert.Equal(405, (await _server.SendAsync("TRACE / HTTP/1.0\r\n\r\n")).Status);
    }
}
