using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.State;
using ErsatzTV.Infrastructure.Streaming.Graphics;
using LanguageExt;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Infrastructure.Tests.Streaming.Graphics;

/// <summary>
///     The degradation contract for a remote watermark image: whatever the fetcher throws, the
///     element disables itself and the stream survives. (ersatztv#511)
/// </summary>
[TestFixture]
public class WatermarkElementRemoteImageTests
{
    private const string RemoteLogo = "https://example.com/logo.png";

    private static readonly Exception[] FetchFailures =
    [
        new TimeoutException("timed out fetching remote image"),
        new InvalidOperationException("remote image exceeds the byte limit"),
        new HttpRequestException("no route to host")
    ];

    [TestCaseSource(nameof(FetchFailures))]
    public async Task Should_Disable_The_Watermark_When_The_Fetch_Fails(Exception failure)
    {
        var fetcher = Substitute.For<IRemoteImageFetcher>();
        fetcher.Fetch(Arg.Any<Uri>(), Arg.Any<CancellationToken>()).Returns<Task<Stream>>(_ => throw failure);

        var element = new WatermarkElement(
            RemoteWatermarkOptions(),
            fetcher,
            Substitute.For<ILogger>());

        // must not throw -- a killed graphics element must never propagate into the stream
        await element.InitializeAsync(Context(), CancellationToken.None);

        element.IsFinished.ShouldBeTrue();
    }

    // the counterpart: caller cancellation is not a fetch failure, but it must still not escape
    // into the streaming pipeline as an unhandled exception
    [Test]
    public async Task Should_Disable_The_Watermark_When_The_Caller_Cancels()
    {
        var fetcher = Substitute.For<IRemoteImageFetcher>();
        fetcher.Fetch(Arg.Any<Uri>(), Arg.Any<CancellationToken>())
            .Returns<Task<Stream>>(_ => throw new OperationCanceledException());

        var element = new WatermarkElement(RemoteWatermarkOptions(), fetcher, Substitute.For<ILogger>());

        await element.InitializeAsync(Context(), CancellationToken.None);

        element.IsFinished.ShouldBeTrue();
    }

    // a remote path really does route to the fetcher -- without this the tests above would pass
    // even if LoadImage stopped recognising http(s) urls
    [Test]
    public async Task Should_Route_A_Remote_Path_Through_The_Fetcher()
    {
        var fetcher = Substitute.For<IRemoteImageFetcher>();
        fetcher.Fetch(Arg.Any<Uri>(), Arg.Any<CancellationToken>())
            .Returns<Task<Stream>>(_ => throw new TimeoutException());

        var element = new WatermarkElement(RemoteWatermarkOptions(), fetcher, Substitute.For<ILogger>());

        await element.InitializeAsync(Context(), CancellationToken.None);

        await fetcher.Received(1).Fetch(new Uri(RemoteLogo), Arg.Any<CancellationToken>());
    }

    // ...and a local path must not touch the network at all
    [Test]
    public async Task Should_Not_Use_The_Fetcher_For_A_Local_Path()
    {
        var fetcher = Substitute.For<IRemoteImageFetcher>();

        var options = new WatermarkOptions(Watermark(), "/no/such/logo.png", Option<int>.None);
        var element = new WatermarkElement(options, fetcher, Substitute.For<ILogger>());

        await element.InitializeAsync(Context(), CancellationToken.None);

        await fetcher.DidNotReceive().Fetch(Arg.Any<Uri>(), Arg.Any<CancellationToken>());
        element.IsFinished.ShouldBeTrue();
    }


    // --- the RETENTION budget is wired in, and is remote-only (M4 from re-review) ---
    //
    // These two differ ONLY in the scale percent, so together they detect deletion of the
    // `if (isRemoteUri) EnsureScaledFramesAffordable(...)` call site: without it the first case
    // would succeed. The source is trivial to decode (300 x 64x64 = 1.2 MP, well inside the decode
    // budget) but retains ~2.5 GB of SKBitmap once every frame is scaled to 1080p.

    [Test]
    public async Task Should_Disable_The_Watermark_When_Scaled_Frames_Blow_The_Retention_Budget()
    {
        var element = new WatermarkElement(
            RemoteWatermarkOptions(widthPercent: 100),
            FetcherReturning(Apng(64, 64, 300)),
            Substitute.For<ILogger>());

        await element.InitializeAsync(Context(), CancellationToken.None);

        element.IsFinished.ShouldBeTrue();
    }

    [Test]
    public async Task Should_Keep_The_Watermark_When_The_Same_Animation_Is_Scaled_Small()
    {
        var element = new WatermarkElement(
            RemoteWatermarkOptions(widthPercent: 10),
            FetcherReturning(Apng(64, 64, 300)),
            Substitute.For<ILogger>());

        await element.InitializeAsync(Context(), CancellationToken.None);

        element.IsFinished.ShouldBeFalse();
    }

    private static IRemoteImageFetcher FetcherReturning(Stream stream)
    {
        var fetcher = Substitute.For<IRemoteImageFetcher>();
        fetcher.Fetch(Arg.Any<Uri>(), Arg.Any<CancellationToken>()).Returns(stream);
        return fetcher;
    }

    private static MemoryStream Apng(int width, int height, int frames)
    {
        using var image = new Image<Rgba32>(width, height);
        for (var i = 1; i < frames; i++)
        {
            image.Frames.CreateFrame();
        }

        var stream = new MemoryStream();
        image.Save(stream, new PngEncoder { ColorType = PngColorType.RgbWithAlpha });
        stream.Position = 0;
        return stream;
    }

    private static WatermarkOptions RemoteWatermarkOptions(double widthPercent = 10) =>
        new(Watermark(widthPercent), RemoteLogo, Option<int>.None);

    private static ChannelWatermark Watermark(double widthPercent = 10) =>
        new()
        {
            Name = "test",
            Mode = ChannelWatermarkMode.Permanent,
            Location = WatermarkLocation.BottomRight,
            Size = WatermarkSize.Scaled,
            WidthPercent = widthPercent,
            HorizontalMarginPercent = 5,
            VerticalMarginPercent = 5,
            Opacity = 100
        };

    private static GraphicsEngineContext Context() =>
        new(
            "1",
            null,
            [],
            [],
            new Resolution { Width = 1920, Height = 1080 },
            new Resolution { Width = 1920, Height = 1080 },
            new FrameRate("30"),
            DateTimeOffset.UnixEpoch,
            DateTimeOffset.UnixEpoch,
            TimeSpan.Zero,
            TimeSpan.FromMinutes(1),
            TimeSpan.FromMinutes(1));
}
