using System.IO.Compression;
using System.Text;
using System.Text.RegularExpressions;
using Sunfish.Auth;
using Sunfish.Config;

namespace Sunfish.Tests;

/// <summary>Escritura por la web contra un servidor real: formularios, PUT, ZIP y usuarios.</summary>
public class WebWriteTests : IAsyncLifetime
{
    private readonly TempDir _rw = new();
    private readonly TempDir _ro = new();
    private readonly TempDir _priv = new();
    private TestServer _server = null!;

    private static readonly string AnaAuth = "Authorization: Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes("ana:1234")) + "\r\n";

    public Task InitializeAsync()
    {
        _rw.File("existe.txt", "original");
        _rw.File("carpeta/dentro.txt", "x");
        _ro.File("solo.txt", "lectura");
        _priv.File("secreto.txt", "s");

        var config = new SunfishConfig();
        config.Users.Add(new UserConfig { Name = "ana", PasswordHash = Passwords.Hash("1234", 1000) });
        config.Shares.Add(new ShareConfig { Name = "rw", Location = "/rw", Path = _rw.Path, Upload = true, Rename = true, Delete = true, Zip = true });
        config.Shares.Add(new ShareConfig { Name = "ro", Location = "/ro", Path = _ro.Path });
        config.Shares.Add(new ShareConfig { Name = "priv", Location = "/priv", Path = _priv.Path, Users = ["ana"] });
        _server = new TestServer(config);
        return Task.CompletedTask;
    }

    public async Task DisposeAsync()
    {
        await _server.DisposeAsync();
        _rw.Dispose();
        _ro.Dispose();
        _priv.Dispose();
    }

    private async Task<string> TokenAsync(string folder, string action)
    {
        var page = await _server.GetAsync(folder);
        var match = Regex.Match(page.BodyText, $"NAME=\"a\" VALUE=\"{action}\"><INPUT TYPE=HIDDEN NAME=\"t\" VALUE=\"([^\"]+)\"");
        Assert.True(match.Success, "no form for " + action);
        return match.Groups[1].Value;
    }

    private Task<RawResponse> PostFormAsync(string folder, string body, string version = "HTTP/1.0") =>
        _server.SendAsync($"POST {folder} {version}\r\nHost: h\r\nConnection: close\r\nContent-Type: application/x-www-form-urlencoded\r\n" +
                          $"Content-Length: {Encoding.Latin1.GetByteCount(body)}\r\n\r\n{body}");

    private Task<RawResponse> PostMultipartAsync(string folder, string token, string fileName, string content)
    {
        string body =
            "--B0und\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nupload\r\n" +
            $"--B0und\r\nContent-Disposition: form-data; name=\"t\"\r\n\r\n{token}\r\n" +
            $"--B0und\r\nContent-Disposition: form-data; name=\"f\"; filename=\"{fileName}\"\r\nContent-Type: application/octet-stream\r\n\r\n{content}\r\n" +
            "--B0und--\r\n";
        return _server.SendAsync($"POST {folder} HTTP/1.0\r\nContent-Type: multipart/form-data; boundary=B0und\r\n" +
                                 $"Content-Length: {Encoding.Latin1.GetByteCount(body)}\r\n\r\n{body}");
    }

    [Fact]
    public async Task ListingShowsFormsOnlyWhenAllowed()
    {
        var rw = await _server.GetAsync("/rw/");
        Assert.Contains("ENCTYPE=\"multipart/form-data\"", rw.BodyText);
        Assert.Contains("?$rename=existe.txt", rw.BodyText);
        Assert.Contains("?$delete=existe.txt", rw.BodyText);
        Assert.Contains("?$zip", rw.BodyText);

        var ro = await _server.GetAsync("/ro/");
        Assert.DoesNotContain("<FORM", ro.BodyText);
        Assert.DoesNotContain("$delete", ro.BodyText);
    }

    [Fact]
    public async Task UploadsWithAClassicForm()
    {
        string token = await TokenAsync("/rw/", "upload");
        var r = await PostMultipartAsync("/rw/", token, "C:\\WINDOWS\\NUEVO.TXT", "subido");
        Assert.Equal(302, r.Status); // HTTP/1.0: 302, no 303
        Assert.EndsWith("/rw/", r.Headers["Location"]);
        Assert.Equal("subido", File.ReadAllText(Path.Combine(_rw.Path, "NUEVO.TXT")));
    }

    [Fact]
    public async Task UploadWithoutValidTokenWritesNothing()
    {
        var r = await PostMultipartAsync("/rw/", "falso", "malo.txt", "x");
        Assert.Equal(403, r.Status);
        Assert.False(File.Exists(Path.Combine(_rw.Path, "malo.txt")));
    }

    [Fact]
    public async Task UploadDoesNotOverwriteByDefault()
    {
        string token = await TokenAsync("/rw/", "upload");
        var r = await PostMultipartAsync("/rw/", token, "existe.txt", "pisado");
        Assert.Equal(409, r.Status);
        Assert.Equal("original", File.ReadAllText(Path.Combine(_rw.Path, "existe.txt")));
    }

    [Fact]
    public async Task ReadOnlySharesRefuseWrites()
    {
        var r = await PostFormAsync("/ro/", "a=mkdir&t=x&name=nueva");
        Assert.Equal(403, r.Status);
        Assert.False(Directory.Exists(Path.Combine(_ro.Path, "nueva")));
        r = await _server.SendAsync("PUT /ro/x.txt HTTP/1.0\r\nContent-Length: 1\r\n\r\nx");
        Assert.Equal(403, r.Status);
    }

    [Fact]
    public async Task CreatesFoldersWithLatin1Names()
    {
        string token = await TokenAsync("/rw/", "mkdir");
        var r = await PostFormAsync("/rw/", $"a=mkdir&t={token}&name=A%F1o+nuevo", "HTTP/1.1");
        Assert.Equal(303, r.Status);
        Assert.True(Directory.Exists(Path.Combine(_rw.Path, "Año nuevo")));
    }

    [Fact]
    public async Task RenamesThroughTheConfirmationPage()
    {
        var page = await _server.GetAsync("/rw/?$rename=existe.txt");
        Assert.Equal(200, page.Status);
        var token = Regex.Match(page.BodyText, "NAME=\"t\" VALUE=\"([^\"]+)\"").Groups[1].Value;
        var r = await PostFormAsync("/rw/", $"a=rename&t={token}&name=existe.txt&to=renombrado.txt");
        Assert.Equal(302, r.Status);
        Assert.True(File.Exists(Path.Combine(_rw.Path, "renombrado.txt")));
        Assert.False(File.Exists(Path.Combine(_rw.Path, "existe.txt")));
    }

    [Fact]
    public async Task DeletesFoldersAfterConfirmation()
    {
        var page = await _server.GetAsync("/rw/?$delete=carpeta");
        Assert.Contains("carpeta", page.BodyText);
        var token = Regex.Match(page.BodyText, "NAME=\"t\" VALUE=\"([^\"]+)\"").Groups[1].Value;
        var r = await PostFormAsync("/rw/", $"a=delete&t={token}&name=carpeta");
        Assert.Equal(302, r.Status);
        Assert.False(Directory.Exists(Path.Combine(_rw.Path, "carpeta")));
    }

    [Fact]
    public async Task TokensAreBoundToTheirAction()
    {
        string mkdirToken = await TokenAsync("/rw/", "mkdir");
        var r = await PostFormAsync("/rw/", $"a=delete&t={mkdirToken}&name=existe.txt");
        Assert.Equal(403, r.Status);
        Assert.True(File.Exists(Path.Combine(_rw.Path, "existe.txt")));
    }

    [Fact]
    public async Task ChunkedPutUploads()
    {
        string head = "PUT /rw/grande.bin HTTP/1.1\r\nHost: h\r\nConnection: close\r\nX-Sunfish-Upload: abcdef0123\r\n";
        var r = await _server.SendAsync(head + "Content-Range: bytes 0-2/6\r\nContent-Length: 3\r\n\r\nabc");
        Assert.Equal(202, r.Status);
        r = await _server.SendAsync(head + "Content-Range: bytes 3-5/6\r\nContent-Length: 3\r\n\r\ndef");
        Assert.Equal(201, r.Status);
        Assert.Equal("abcdef", File.ReadAllText(Path.Combine(_rw.Path, "grande.bin")));
    }

    [Fact]
    public async Task PlainPutUploadsTheWholeFile()
    {
        var r = await _server.SendAsync("PUT /rw/entero.txt HTTP/1.1\r\nHost: h\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntodo\r\n0\r\n\r\n");
        Assert.Equal(201, r.Status);
        Assert.Equal("todo", File.ReadAllText(Path.Combine(_rw.Path, "entero.txt")));
        r = await _server.SendAsync("PUT /rw/entero.txt HTTP/1.0\r\nContent-Length: 1\r\n\r\nx");
        Assert.Equal(409, r.Status); // sin Overwrite
    }

    [Fact]
    public async Task DownloadsFoldersAsZip()
    {
        var r = await _server.GetAsync("/rw/?$zip");
        Assert.Equal(200, r.Status);
        Assert.Equal("application/zip", r.Headers["Content-Type"]);
        Assert.Equal(r.Body.Length.ToString(), r.Headers["Content-Length"]);
        using var archive = new ZipArchive(new MemoryStream(r.Body));
        Assert.Contains(archive.Entries, e => e.FullName == "carpeta/dentro.txt");
        Assert.Equal(403, (await _server.GetAsync("/ro/?$zip")).Status);
    }

    [Fact]
    public async Task ProtectedSharesNeedBasicAuth()
    {
        var r = await _server.GetAsync("/priv/secreto.txt");
        Assert.Equal(401, r.Status);
        Assert.StartsWith("Basic realm=", r.Headers["WWW-Authenticate"]);

        r = await _server.GetAsync("/priv/secreto.txt", extraHeaders: AnaAuth);
        Assert.Equal(200, r.Status);
        Assert.Equal("s", r.BodyText);

        string wrong = "Authorization: Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes("ana:mal")) + "\r\n";
        Assert.Equal(401, (await _server.GetAsync("/priv/", extraHeaders: wrong)).Status);
    }
}
