using System.Diagnostics;
using System.Net;

namespace ErsatzTV.Infrastructure.Tests.Streaming;

/// <summary>
///     Shared fakes for the HTTP-backed streaming services. Extracted from
///     <see cref="HttpRemoteStreamProberTests" /> when <see cref="HttpRemoteImageFetcherTests" />
///     needed the same harness. (ersatztv#511)
/// </summary>
internal sealed class StubHttpClientFactory(HttpMessageHandler handler) : IHttpClientFactory
{
    public HttpClient CreateClient(string name) => new(handler, disposeHandler: false);
}

internal sealed class StatusCodeHttpMessageHandler(HttpStatusCode statusCode, string finalUri = null)
    : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        // HttpClient rewrites RequestMessage.RequestUri to the final hop when it follows a
        // redirect; finalUri lets a test stand in for "the media server answered this".
        if (finalUri is not null)
        {
            request.RequestUri = new Uri(finalUri);
        }

        return Task.FromResult(new HttpResponseMessage(statusCode) { RequestMessage = request });
    }
}

internal sealed class FixedResponseHttpMessageHandler(HttpResponseMessage response) : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        response.RequestMessage = request;
        return Task.FromResult(response);
    }
}

/// <summary>A readable stream that records how many bytes were actually pulled from it.</summary>
internal sealed class TrackingStream(long length) : Stream
{
    public int BytesRead { get; private set; }

    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => false;
    public override long Length => length;

    public override long Position
    {
        get => BytesRead;
        set => throw new NotSupportedException();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        if (BytesRead >= length)
        {
            return 0;
        }

        int toRead = (int)Math.Min(count, length - BytesRead);
        Array.Clear(buffer, offset, toRead);
        BytesRead += toRead;
        return toRead;
    }

    public override void Flush()
    {
    }

    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}

/// <summary>
///     A readable stream that returns headers-worth of data instantly and then drips forever —
///     the case that <see cref="HttpCompletionOption.ResponseHeadersRead" /> leaves outside
///     <see cref="HttpClient.Timeout" />.
/// </summary>
internal sealed class SlowDripStream : Stream
{
    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => false;
    public override long Length => throw new NotSupportedException();

    public override long Position
    {
        get => 0;
        set => throw new NotSupportedException();
    }

    public override async ValueTask<int> ReadAsync(
        Memory<byte> buffer,
        CancellationToken cancellationToken = default)
    {
        await Task.Delay(Timeout.Infinite, cancellationToken);
        throw new UnreachableException();
    }

    public override int Read(byte[] buffer, int offset, int count) =>
        ReadAsync(buffer.AsMemory(offset, count), CancellationToken.None).AsTask().GetAwaiter().GetResult();

    public override void Flush()
    {
    }

    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}

internal sealed class ThrowingHttpMessageHandler(Exception exception) : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) =>
        Task.FromException<HttpResponseMessage>(exception);
}

/// <summary>
///     A handler that never completes until the request is cancelled — stands in for a host that
///     accepts the connection and then hangs.
/// </summary>
internal sealed class HangingHttpMessageHandler : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        await Task.Delay(Timeout.Infinite, cancellationToken);
        throw new UnreachableException();
    }
}

/// <summary>
///     A readable stream that yields <paramref name="length" /> bytes but only ever a little at a
///     time, so a size cap has to be enforced during the copy rather than from Content-Length.
/// </summary>
internal sealed class ChunkedZeroStream(long length, int chunkSize = 4096) : Stream
{
    private long _position;

    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => false;
    public override long Length => length;

    public override long Position
    {
        get => _position;
        set => throw new NotSupportedException();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        if (_position >= length)
        {
            return 0;
        }

        int toRead = (int)Math.Min(Math.Min(count, chunkSize), length - _position);
        Array.Clear(buffer, offset, toRead);
        _position += toRead;
        return toRead;
    }

    public override void Flush()
    {
    }

    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
