using System.Net;
using System.Net.Sockets;
using System.Text;
using Sunfish.Config;
using Sunfish.Host;

namespace Sunfish.Tests;

/// <summary>
/// Las variables de entorno son del proceso entero: las pruebas que las tocan
/// comparten esta coleccion para que xUnit no las ejecute en paralelo.
/// </summary>
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class EnvironmentCollection
{
    public const string Name = "Environment";
}

/// <summary>Fija variables de entorno y las restaura al terminar.</summary>
internal sealed class EnvScope : IDisposable
{
    private readonly Dictionary<string, string?> _saved = new();

    public EnvScope Set(string name, string? value)
    {
        if (!_saved.ContainsKey(name))
            _saved[name] = Environment.GetEnvironmentVariable(name);
        Environment.SetEnvironmentVariable(name, value);
        return this;
    }

    public void Dispose()
    {
        foreach (var (name, value) in _saved)
            Environment.SetEnvironmentVariable(name, value);
    }
}

/// <summary>Carpeta temporal que se borra al terminar.</summary>
internal sealed class TempDir : IDisposable
{
    public string Path { get; } =
        System.IO.Path.Combine(System.IO.Path.GetTempPath(), "sunfish-tests-" + Guid.NewGuid().ToString("N"));

    public TempDir() => Directory.CreateDirectory(Path);

    public string File(string name, string content = "")
    {
        string full = System.IO.Path.Combine(Path, name);
        Directory.CreateDirectory(System.IO.Path.GetDirectoryName(full)!);
        System.IO.File.WriteAllText(full, content);
        return full;
    }

    public string Folder(string name)
    {
        string full = System.IO.Path.Combine(Path, name);
        Directory.CreateDirectory(full);
        return full;
    }

    public void Dispose()
    {
        try { Directory.Delete(Path, recursive: true); }
        catch (IOException) { }
        catch (UnauthorizedAccessException) { }
    }
}

/// <summary>Servidor real en 127.0.0.1 con un puerto libre, y un cliente HTTP en bruto.</summary>
internal sealed class TestServer : IAsyncDisposable
{
    private readonly SunfishHost _host;

    public TestServer(SunfishConfig config)
    {
        config.Server.Bind = [IPAddress.Loopback];
        config.Server.HttpPort = 0;
        config.Server.FtpPort = 0;
        config.Server.Announce = false;
        _host = new SunfishHost(config);
        _host.Start();
    }

    public SunfishHost Host => _host;
    public int Port => _host.HttpPort;
    public int FtpPort => _host.FtpPort;

    /// <summary>Envia la peticion tal cual y devuelve todo lo recibido hasta el cierre.</summary>
    public async Task<RawResponse> SendAsync(string request)
    {
        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, Port);
        var stream = client.GetStream();
        await stream.WriteAsync(Encoding.Latin1.GetBytes(request));
        var ms = new MemoryStream();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        await stream.CopyToAsync(ms, cts.Token);
        return new RawResponse(ms.ToArray());
    }

    /// <summary>GET con HTTP/1.0 y cierre de conexion.</summary>
    public Task<RawResponse> GetAsync(string path, string? userAgent = null, string extraHeaders = "") =>
        SendAsync($"GET {path} HTTP/1.0\r\n" +
                  (userAgent is null ? "" : $"User-Agent: {userAgent}\r\n") +
                  extraHeaders + "\r\n");

    public async ValueTask DisposeAsync() => await _host.DisposeAsync();
}

internal sealed class RawResponse
{
    public RawResponse(byte[] data)
    {
        Data = data;
        string text = Encoding.Latin1.GetString(data);
        int end = text.IndexOf("\r\n\r\n", StringComparison.Ordinal);
        if (text.StartsWith("HTTP/", StringComparison.Ordinal) && end >= 0)
        {
            var lines = text[..end].Split("\r\n");
            Status = int.Parse(lines[0].Split(' ')[1]);
            foreach (var line in lines.Skip(1))
            {
                int colon = line.IndexOf(':');
                Headers[line[..colon].Trim()] = line[(colon + 1)..].Trim();
            }
            Body = data[(end + 4)..];
        }
        else
        {
            Body = data; // HTTP/0.9: sin cabeceras
        }
    }

    public byte[] Data { get; }
    public int Status { get; }
    public Dictionary<string, string> Headers { get; } = new(StringComparer.OrdinalIgnoreCase);
    public byte[] Body { get; }
    public string BodyText => Encoding.Latin1.GetString(Body);
}
