using System.Net;
using System.Security.Cryptography;
using Metin2Launcher.Transfer;
using Xunit;

namespace Metin2Launcher.Tests;

public class BlobDownloaderTests : IDisposable
{
    private readonly string _tmp;

    public BlobDownloaderTests()
    {
        _tmp = Path.Combine(Path.GetTempPath(), "metin-blob-" + Guid.NewGuid());
        Directory.CreateDirectory(_tmp);
    }

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

    /// <summary>Minimal HttpListener-based test server. Serves blobs at /{hash[0:2]}/{hash}.</summary>
    private sealed class BlobServer : IDisposable
    {
        private readonly HttpListener _listener;
        private readonly CancellationTokenSource _cts = new();
        private readonly Task _loop;

        public string BaseUrl { get; }
        public Func<HttpListenerRequest, HttpListenerResponse, Task>? Handler { get; set; }

        public BlobServer()
        {
            var port = GetFreeTcpPort();
            BaseUrl = $"http://127.0.0.1:{port}";
            _listener = new HttpListener();
            _listener.Prefixes.Add(BaseUrl + "/");
            _listener.Start();
            _loop = Task.Run(RunAsync);
        }

        private async Task RunAsync()
        {
            while (!_cts.IsCancellationRequested)
            {
                HttpListenerContext ctx;
                try { ctx = await _listener.GetContextAsync().ConfigureAwait(false); }
                catch { return; }
                _ = Task.Run(async () =>
                {
                    try
                    {
                        if (Handler is not null)
                            await Handler(ctx.Request, ctx.Response).ConfigureAwait(false);
                    }
                    catch { }
                    finally
                    {
                        try { ctx.Response.Close(); } catch { }
                    }
                });
            }
        }

        public void Dispose()
        {
            _cts.Cancel();
            try { _listener.Stop(); } catch { }
            try { _loop.Wait(500); } catch { }
            try { _listener.Close(); } catch { }
        }

        private static int GetFreeTcpPort()
        {
            var l = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0);
            l.Start();
            var port = ((System.Net.IPEndPoint)l.LocalEndpoint).Port;
            l.Stop();
            return port;
        }
    }

    private static string Sha256Hex(byte[] data)
        => Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant();

    [Fact]
    public async Task Downloads_full_blob_and_matches_hash()
    {
        var payload = new byte[16 * 1024];
        new Random(1).NextBytes(payload);
        var hash = Sha256Hex(payload);

        using var server = new BlobServer();
        server.Handler = async (req, resp) =>
        {
            resp.StatusCode = 200;
            resp.ContentLength64 = payload.Length;
            await resp.OutputStream.WriteAsync(payload);
        };

        using var http = new HttpClient();
        var dl = new BlobDownloader(http, server.BaseUrl, maxRetries: 3);
        var dest = Path.Combine(_tmp, "full.bin");
        await dl.DownloadAsync(hash, payload.Length, dest, CancellationToken.None);

        Assert.Equal(hash, FileHasher.HashFileOrNull(dest));
    }

    [Fact]
    public async Task Resumes_partial_download_via_range()
    {
        var payload = new byte[8 * 1024];
        new Random(2).NextBytes(payload);
        var hash = Sha256Hex(payload);

        var rangeSeen = false;
        using var server = new BlobServer();
        server.Handler = async (req, resp) =>
        {
            var range = req.Headers["Range"];
            if (!string.IsNullOrEmpty(range))
            {
                rangeSeen = true;
                // parse "bytes=N-"
                var from = long.Parse(range.Replace("bytes=", "").TrimEnd('-'));
                resp.StatusCode = 206;
                resp.ContentLength64 = payload.Length - from;
                resp.AddHeader("Content-Range", $"bytes {from}-{payload.Length - 1}/{payload.Length}");
                await resp.OutputStream.WriteAsync(payload.AsMemory((int)from));
            }
            else
            {
                resp.StatusCode = 200;
                resp.ContentLength64 = payload.Length;
                await resp.OutputStream.WriteAsync(payload);
            }
        };

        var dest = Path.Combine(_tmp, "resume.bin");
        // Pre-seed with the first half of the payload to simulate a prior interrupted download.
        File.WriteAllBytes(dest, payload.Take(payload.Length / 2).ToArray());

        using var http = new HttpClient();
        var dl = new BlobDownloader(http, server.BaseUrl, maxRetries: 3);
        await dl.DownloadAsync(hash, payload.Length, dest, CancellationToken.None);

        Assert.True(rangeSeen, "server should have seen a Range header");
        Assert.Equal(hash, FileHasher.HashFileOrNull(dest));
    }

    [Fact]
    public async Task Retries_and_fails_on_persistent_hash_mismatch()
    {
        var realPayload = new byte[1024];
        new Random(3).NextBytes(realPayload);
        var expectedHash = Sha256Hex(realPayload);

        var corrupted = (byte[])realPayload.Clone();
        corrupted[0] ^= 0xFF;

        var attempts = 0;
        using var server = new BlobServer();
        server.Handler = async (req, resp) =>
        {
            Interlocked.Increment(ref attempts);
            resp.StatusCode = 200;
            resp.ContentLength64 = corrupted.Length;
            await resp.OutputStream.WriteAsync(corrupted);
        };

        using var http = new HttpClient();
        var dl = new BlobDownloader(http, server.BaseUrl, maxRetries: 3);
        var dest = Path.Combine(_tmp, "bad.bin");

        await Assert.ThrowsAsync<BlobDownloadException>(() =>
            dl.DownloadAsync(expectedHash, corrupted.Length, dest, CancellationToken.None));

        Assert.Equal(3, attempts);
        Assert.False(File.Exists(dest), "corrupt staging file should be deleted");
    }

    [Fact]
    public void BlobUrl_is_content_addressed_with_two_char_prefix()
    {
        var url = BlobDownloader.BlobUrl("https://example.com/files", "abcdef0123456789");
        Assert.Equal("https://example.com/files/ab/abcdef0123456789", url);
    }
}
