using Microsoft.Extensions.Logging.Abstractions;
using System.Net;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Deployment;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Deployment;

/// <summary>
/// Unit tests for <see cref="HttpDeploymentConfigFetcher"/> using a stub
/// <see cref="HttpMessageHandler"/>. No live HTTP connections are made.
/// </summary>
public class HttpDeploymentConfigFetcherTests
{
    private static HttpDeploymentConfigFetcher CreateFetcher(HttpMessageHandler handler)
    {
        var client = new HttpClient(handler);
        return new HttpDeploymentConfigFetcher(client, NullLogger<HttpDeploymentConfigFetcher>.Instance);
    }

    // -------------------------------------------------------------------------
    // Happy path
    // -------------------------------------------------------------------------

    [Fact]
    public async Task FetchAsync_200_ReturnsBody()
    {
        const string expectedBody = """{"instanceUniqueName":"test"}""";
        var handler = new StubHandler(HttpStatusCode.OK, expectedBody);
        var fetcher = CreateFetcher(handler);

        var result = await fetcher.FetchAsync("http://central", "dep-1", "tok-abc", CancellationToken.None);

        Assert.Equal(expectedBody, result);
    }

    [Fact]
    public async Task FetchAsync_200_RequestUrlIsCorrect()
    {
        var handler = new StubHandler(HttpStatusCode.OK, "{}");
        var fetcher = CreateFetcher(handler);

        await fetcher.FetchAsync("http://central", "dep-abc", "tok", CancellationToken.None);

        Assert.NotNull(handler.LastRequest);
        Assert.Equal(
            "http://central/api/internal/deployments/dep-abc/config",
            handler.LastRequest!.RequestUri!.ToString());
    }

    [Fact]
    public async Task FetchAsync_200_SetsDeploymentTokenHeader()
    {
        var handler = new StubHandler(HttpStatusCode.OK, "{}");
        var fetcher = CreateFetcher(handler);

        await fetcher.FetchAsync("http://central", "dep-1", "my-secret-token", CancellationToken.None);

        Assert.NotNull(handler.LastRequest);
        Assert.True(handler.LastRequest!.Headers.Contains("X-Deployment-Token"));
        Assert.Equal("my-secret-token", handler.LastRequest.Headers.GetValues("X-Deployment-Token").Single());
    }

    [Fact]
    public async Task FetchAsync_BaseUrlWithTrailingSlash_NoDoubleSlashInUrl()
    {
        var handler = new StubHandler(HttpStatusCode.OK, "{}");
        var fetcher = CreateFetcher(handler);

        await fetcher.FetchAsync("http://central/", "dep-1", "tok", CancellationToken.None);

        Assert.NotNull(handler.LastRequest);
        var url = handler.LastRequest!.RequestUri!.ToString();
        Assert.DoesNotContain("//api", url);
        Assert.Equal("http://central/api/internal/deployments/dep-1/config", url);
    }

    [Fact]
    public async Task FetchAsync_DeploymentIdNeedingEscaping_EscapesOnceInUrl()
    {
        var handler = new StubHandler(HttpStatusCode.OK, "{}");
        var fetcher = CreateFetcher(handler);

        // A deploymentId containing a space and a slash — both require escaping.
        await fetcher.FetchAsync("http://central", "dep a/b", "tok", CancellationToken.None);

        Assert.NotNull(handler.LastRequest);
        // Uri.OriginalString preserves the escaped form as built (no normalization/decoding).
        var url = handler.LastRequest!.RequestUri!.OriginalString;
        Assert.Equal("http://central/api/internal/deployments/dep%20a%2Fb/config", url);
        // Not double-encoded: the percent-sign itself must not be re-escaped to %25.
        Assert.DoesNotContain("%25", url);
    }

    // -------------------------------------------------------------------------
    // 404 → IsSuperseded = true
    // -------------------------------------------------------------------------

    [Fact]
    public async Task FetchAsync_404_ThrowsWithIsSupersededTrue()
    {
        var handler = new StubHandler(HttpStatusCode.NotFound, "Not found");
        var fetcher = CreateFetcher(handler);

        var ex = await Assert.ThrowsAsync<DeploymentConfigFetchException>(
            () => fetcher.FetchAsync("http://central", "dep-gone", "tok", CancellationToken.None));

        Assert.True(ex.IsSuperseded);
        Assert.Contains("dep-gone", ex.Message);
    }

    // -------------------------------------------------------------------------
    // Other HTTP errors → IsSuperseded = false
    // -------------------------------------------------------------------------

    [Fact]
    public async Task FetchAsync_401_ThrowsWithIsSupersededFalse()
    {
        var handler = new StubHandler(HttpStatusCode.Unauthorized, "Unauthorized");
        var fetcher = CreateFetcher(handler);

        var ex = await Assert.ThrowsAsync<DeploymentConfigFetchException>(
            () => fetcher.FetchAsync("http://central", "dep-1", "bad-tok", CancellationToken.None));

        Assert.False(ex.IsSuperseded);
        Assert.Contains("401", ex.Message);
    }

    [Fact]
    public async Task FetchAsync_500_ThrowsWithIsSupersededFalse()
    {
        var handler = new StubHandler(HttpStatusCode.InternalServerError, "Server error");
        var fetcher = CreateFetcher(handler);

        var ex = await Assert.ThrowsAsync<DeploymentConfigFetchException>(
            () => fetcher.FetchAsync("http://central", "dep-1", "tok", CancellationToken.None));

        Assert.False(ex.IsSuperseded);
        Assert.Contains("500", ex.Message);
    }

    // -------------------------------------------------------------------------
    // Transport failure → DeploymentConfigFetchException (not raw HttpRequestException)
    // -------------------------------------------------------------------------

    [Fact]
    public async Task FetchAsync_TransportException_WrapsAsDeploymentConfigFetchException()
    {
        var handler = new ThrowingHandler(new HttpRequestException("Connection refused"));
        var fetcher = CreateFetcher(handler);

        var ex = await Assert.ThrowsAsync<DeploymentConfigFetchException>(
            () => fetcher.FetchAsync("http://central", "dep-1", "tok", CancellationToken.None));

        Assert.False(ex.IsSuperseded);
        Assert.IsType<HttpRequestException>(ex.InnerException);
        Assert.Contains("Connection refused", ex.Message);
    }

    [Fact]
    public async Task FetchAsync_HttpClientTimeout_WrapsAsTransportError_NotCancellation()
    {
        // HttpClient.Timeout surfaces as TaskCanceledException while the caller's ct is NOT cancelled.
        var handler = new ThrowingHandler(new TaskCanceledException("simulated HttpClient timeout"));
        var fetcher = CreateFetcher(handler);

        var ex = await Assert.ThrowsAsync<DeploymentConfigFetchException>(
            () => fetcher.FetchAsync("http://central", "dep-1", "tok", CancellationToken.None));

        Assert.False(ex.IsSuperseded);
        Assert.IsType<TaskCanceledException>(ex.InnerException);
    }

    // -------------------------------------------------------------------------
    // Cancellation propagates as-is
    // -------------------------------------------------------------------------

    [Fact]
    public async Task FetchAsync_CancelledToken_PropagatesOperationCanceledException()
    {
        using var cts = new CancellationTokenSource();
        cts.Cancel();

        var handler = new DelayHandler(); // never returns
        var fetcher = CreateFetcher(handler);

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            () => fetcher.FetchAsync("http://central", "dep-1", "tok", cts.Token));
    }

    // -------------------------------------------------------------------------
    // Stub handlers
    // -------------------------------------------------------------------------

    private sealed class StubHandler : HttpMessageHandler
    {
        private readonly HttpStatusCode _status;
        private readonly string _body;
        public HttpRequestMessage? LastRequest { get; private set; }

        public StubHandler(HttpStatusCode status, string body)
        {
            _status = status;
            _body = body;
        }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
        {
            LastRequest = request;
            var resp = new HttpResponseMessage(_status)
            {
                Content = new StringContent(_body)
            };
            return Task.FromResult(resp);
        }
    }

    private sealed class ThrowingHandler : HttpMessageHandler
    {
        private readonly Exception _ex;
        public ThrowingHandler(Exception ex) => _ex = ex;

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
            => throw _ex;
    }

    private sealed class DelayHandler : HttpMessageHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
        {
            await Task.Delay(Timeout.Infinite, ct);
            return new HttpResponseMessage(HttpStatusCode.OK);
        }
    }
}
