using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Text;

namespace StellaOps.Feedser.Source.Common.Testing;

/// <summary>
/// Deterministic HTTP handler used by tests to supply canned responses keyed by request URI and method.
/// Tracks requests for assertions and supports fallbacks/exceptions.
/// </summary>
public sealed class CannedHttpMessageHandler : HttpMessageHandler
{
    private readonly ConcurrentDictionary<RequestKey, ConcurrentQueue<Func<HttpRequestMessage, HttpResponseMessage>>> _responses =
        new(RequestKeyComparer.Instance);

    private readonly ConcurrentQueue<CannedRequestRecord> _requests = new();

    private Func<HttpRequestMessage, HttpResponseMessage>? _fallback;

    /// <summary>
    /// Recorded requests in arrival order.
    /// </summary>
    public IReadOnlyCollection<CannedRequestRecord> Requests => _requests.ToArray();

    /// <summary>
    /// Registers a canned response for a GET request to <paramref name="requestUri"/>.
    /// </summary>
    public void AddResponse(Uri requestUri, Func<HttpResponseMessage> factory)
        => AddResponse(HttpMethod.Get, requestUri, _ => factory());

    /// <summary>
    /// Registers a canned response for the specified method and URI.
    /// </summary>
    public void AddResponse(HttpMethod method, Uri requestUri, Func<HttpResponseMessage> factory)
        => AddResponse(method, requestUri, _ => factory());

    /// <summary>
    /// Registers a canned response using the full request context.
    /// </summary>
    public void AddResponse(HttpMethod method, Uri requestUri, Func<HttpRequestMessage, HttpResponseMessage> factory)
    {
        ArgumentNullException.ThrowIfNull(method);
        ArgumentNullException.ThrowIfNull(requestUri);
        ArgumentNullException.ThrowIfNull(factory);

        var key = new RequestKey(method, requestUri);
        var queue = _responses.GetOrAdd(key, static _ => new ConcurrentQueue<Func<HttpRequestMessage, HttpResponseMessage>>());
        queue.Enqueue(factory);
    }

    /// <summary>
    /// Registers an exception to be thrown for the specified request.
    /// </summary>
    public void AddException(HttpMethod method, Uri requestUri, Exception exception)
    {
        ArgumentNullException.ThrowIfNull(exception);
        AddResponse(method, requestUri, _ => throw exception);
    }

    /// <summary>
    /// Registers a fallback used when no specific response is queued for a request.
    /// </summary>
    public void SetFallback(Func<HttpRequestMessage, HttpResponseMessage> fallback)
    {
        ArgumentNullException.ThrowIfNull(fallback);
        _fallback = fallback;
    }

    /// <summary>
    /// Clears registered responses and captured requests.
    /// </summary>
    public void Clear()
    {
        _responses.Clear();
        while (_requests.TryDequeue(out _))
        {
        }
        _fallback = null;
    }

    /// <summary>
    /// Throws if any responses remain queued.
    /// </summary>
    public void AssertNoPendingResponses()
    {
        foreach (var queue in _responses.Values)
        {
            if (!queue.IsEmpty)
            {
                throw new InvalidOperationException("Not all canned responses were consumed.");
            }
        }
    }

    /// <summary>
    /// Creates an <see cref="HttpClient"/> wired to this handler.
    /// </summary>
    public HttpClient CreateClient()
        => new(this, disposeHandler: false)
        {
            Timeout = TimeSpan.FromSeconds(10),
        };

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.RequestUri is null)
        {
            throw new InvalidOperationException("Request URI is required for canned responses.");
        }

        var key = new RequestKey(request.Method ?? HttpMethod.Get, request.RequestUri);
        var factory = DequeueFactory(key);

        if (factory is null)
        {
            if (_fallback is null)
            {
                throw new InvalidOperationException($"No canned response registered for {request.Method} {request.RequestUri}.");
            }

            factory = _fallback;
        }

        var snapshot = CaptureRequest(request);
        _requests.Enqueue(snapshot);

        var response = factory(request);
        response.RequestMessage ??= request;
        return Task.FromResult(response);
    }

    private Func<HttpRequestMessage, HttpResponseMessage>? DequeueFactory(RequestKey key)
    {
        if (_responses.TryGetValue(key, out var queue) && queue.TryDequeue(out var factory))
        {
            return factory;
        }

        return null;
    }

    private static CannedRequestRecord CaptureRequest(HttpRequestMessage request)
    {
        var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        foreach (var header in request.Headers)
        {
            headers[header.Key] = string.Join(',', header.Value);
        }

        if (request.Content is not null)
        {
            foreach (var header in request.Content.Headers)
            {
                headers[header.Key] = string.Join(',', header.Value);
            }
        }

        return new CannedRequestRecord(
            Timestamp: DateTimeOffset.UtcNow,
            Method: request.Method ?? HttpMethod.Get,
            Uri: request.RequestUri!,
            Headers: headers);
    }

    private readonly record struct RequestKey(HttpMethod Method, string Uri)
    {
        public RequestKey(HttpMethod method, Uri uri)
            : this(method, uri.ToString())
        {
        }

        public bool Equals(RequestKey other)
            => string.Equals(Method.Method, other.Method.Method, StringComparison.OrdinalIgnoreCase)
               && string.Equals(Uri, other.Uri, StringComparison.OrdinalIgnoreCase);

        public override int GetHashCode()
        {
            var methodHash = StringComparer.OrdinalIgnoreCase.GetHashCode(Method.Method);
            var uriHash = StringComparer.OrdinalIgnoreCase.GetHashCode(Uri);
            return HashCode.Combine(methodHash, uriHash);
        }
    }

    private sealed class RequestKeyComparer : IEqualityComparer<RequestKey>
    {
        public static readonly RequestKeyComparer Instance = new();

        public bool Equals(RequestKey x, RequestKey y) => x.Equals(y);

        public int GetHashCode(RequestKey obj) => obj.GetHashCode();
    }

    public readonly record struct CannedRequestRecord(DateTimeOffset Timestamp, HttpMethod Method, Uri Uri, IReadOnlyDictionary<string, string> Headers);

    private static HttpResponseMessage BuildTextResponse(HttpStatusCode statusCode, string content, string contentType)
    {
        var message = new HttpResponseMessage(statusCode)
        {
            Content = new StringContent(content, Encoding.UTF8, contentType),
        };
        return message;
    }

    public void AddJsonResponse(Uri requestUri, string json, HttpStatusCode statusCode = HttpStatusCode.OK)
        => AddResponse(requestUri, () => BuildTextResponse(statusCode, json, "application/json"));

    public void AddTextResponse(Uri requestUri, string content, string contentType = "text/plain", HttpStatusCode statusCode = HttpStatusCode.OK)
        => AddResponse(requestUri, () => BuildTextResponse(statusCode, content, contentType));
}
