using System.Diagnostics;
using System.Threading.Channels;
using Xunit;
using ytLive.Models;
using ytLive.Services;
using ytLive.Services.Encoder;

namespace ytLive.Tests;

/// <summary>
/// The FFmpeg subprocess encoder (TASK 4 ship step 3). The integration test drives
/// the full lifecycle against fakes: encoder probe → subprocess start with the right
/// args → raw frames into stdin → stderr progress parsed into health → graceful stop.
/// The units pin down the pure pieces (args, progress parser, encoder picker) and
/// the failure edges. No real ffmpeg binary, no network.
/// </summary>
public class FfmpegEncoderTests
{
    private sealed class QueuedReader : TextReader
    {
        private readonly Channel<string?> _channel = Channel.CreateUnbounded<string?>();
        public void Enqueue(string s) => _channel.Writer.TryWrite(s);
        public void Complete() => _channel.Writer.TryComplete();
        public override string? ReadLine() => ReadLineAsync().GetAwaiter().GetResult();

        public override async Task<string?> ReadLineAsync()
        {
            try
            {
                return await _channel.Reader.ReadAsync().AsTask().ConfigureAwait(false);
            }
            catch (ChannelClosedException)
            {
                return null; // stream EOF — the process exited and the pipe closed
            }
        }
    }

    private sealed class FakeEncoderProcess : IEncoderProcess
    {
        public ProcessStartInfo? StartInfo { get; private set; }
        public MemoryStream Stdin { get; } = new();
        public Stream StandardInput => Stdin;
        public TextReader StandardOutput { get; }
        public bool HasExited { get; private set; }
        public int ExitCode { get; private set; }
        public bool Killed { get; private set; }
        public bool Started { get; private set; }

        private readonly TaskCompletionSource _exit =
            new(TaskCreationOptions.RunContinuationsAsynchronously);
        private readonly QueuedReader _error = new();

        public FakeEncoderProcess(string probeOutput = "")
            => StandardOutput = new StringReader(probeOutput);

        public TextReader StandardError => _error;
        public void EnqueueStderr(string line) => _error.Enqueue(line);

        public void Start(ProcessStartInfo startInfo)
        {
            StartInfo = startInfo;
            Started = true;
        }

        public void SignalExit(int code = 0)
        {
            ExitCode = code;
            HasExited = true;
            _error.Complete();
            _exit.TrySetResult();
        }

        public void Kill()
        {
            Killed = true;
            _error.Complete();
            _exit.TrySetResult();
        }

        public Task WaitForExitAsync(CancellationToken cancellationToken = default) => _exit.Task;

        public void Dispose()
        {
            _error.Complete();
            _exit.TrySetResult();
        }
    }

    private sealed class FakeEncoderProcessFactory
    {
        private readonly Queue<FakeEncoderProcess> _processes = new();
        public void Return(FakeEncoderProcess p) => _processes.Enqueue(p);
        public FakeEncoderProcess Create() => _processes.Dequeue();
    }

    private sealed class StubLocator : IFfmpegLocator
    {
        public Task<string> LocateAsync(CancellationToken cancellationToken = default)
            => Task.FromResult(@"C:\tools\ffmpeg.exe");
    }

    private static byte[] BgraFrame(int w, int h, byte b, byte g, byte r)
    {
        var bytes = new byte[w * h * 4];
        for (var i = 0; i < bytes.Length; i += 4)
        {
            bytes[i] = b;
            bytes[i + 1] = g;
            bytes[i + 2] = r;
            bytes[i + 3] = 255;
        }
        return bytes;
    }

    [Fact]
    public async Task Start_ProbesEncoder_FeedsFrames_ParsesHealth_StopsGracefully()
    {
        const string encoders =
            " V..... h264_nvenc  NVIDIA NVENC H.264 encoder (codec h264)\n" +
            " V..... libopenh264  OpenH264 H.264 (codec h264)\n";
        var probe = new FakeEncoderProcess(encoders);
        var encoderProc = new FakeEncoderProcess();
        var factory = new FakeEncoderProcessFactory();
        factory.Return(probe);
        factory.Return(encoderProc);

        using var encoder = new FfmpegEncoder(new StubLocator(), factory.Create);

        var health = new TaskCompletionSource<StreamHealth>(TaskCreationOptions.RunContinuationsAsynchronously);
        encoder.HealthUpdated += (_, h) => health.TrySetResult(h);

        var options = new EncoderOptions
        {
            RtmpUrl = "rtmp://a.rtmp.youtube.com/live2/abc-xyz",
            Width = 1920,
            Height = 1080,
            Fps = 60,
            BitrateKbps = 8000,
        };

        await encoder.StartAsync(options);

        Assert.True(probe.Started, "the -encoders probe must run");
        Assert.True(encoderProc.Started, "the encoder subprocess must start");
        Assert.Equal(@"C:\tools\ffmpeg.exe", encoderProc.StartInfo!.FileName);
        var args = encoderProc.StartInfo.ArgumentList.ToArray();
        var c = Array.IndexOf(args, "-c:v");
        Assert.True(
            args[c + 1] == "h264_nvenc",
            "hardware NVENC must be preferred over the listed openh264");

        await encoder.SubmitFrameAsync(new VideoFrame(1920, 1080, BgraFrame(1920, 1080, 255, 0, 0)));
        await encoder.SubmitFrameAsync(new VideoFrame(1920, 1080, BgraFrame(1920, 1080, 0, 255, 0)));
        Assert.Equal(2 * 1920 * 1080 * 4, encoderProc.Stdin.Length);

        encoderProc.EnqueueStderr(
            "frame= 120 fps= 59.9 q=28.0 size= 1024KiB time=00:00:02.00 bitrate= 4000.1kbits/s speed=1.00x");
        var h = await health.Task.WaitAsync(TimeSpan.FromSeconds(5));
        Assert.Equal(4000.1, h.CurrentBitrate, 1);
        Assert.Equal(59.9, h.FPS, 1);
        Assert.Equal(TimeSpan.FromSeconds(2), h.StreamDuration);

        encoderProc.SignalExit();
        await encoder.StopAsync();
        Assert.False(encoder.IsRunning);
        Assert.False(encoderProc.Killed, "graceful stop must not kill the process");
        Assert.Equal(StreamStatus.Offline, h.Status);
    }

    [Fact]
    public async Task Start_WithoutRtmpUrl_Throws()
    {
        using var encoder = new FfmpegEncoder(new StubLocator());
        await Assert.ThrowsAsync<ArgumentException>(() => encoder.StartAsync(new EncoderOptions()));
    }

    [Fact]
    public async Task SubmitFrame_WhenNotRunning_Throws()
    {
        using var encoder = new FfmpegEncoder(new StubLocator());
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => encoder.SubmitFrameAsync(new VideoFrame(2, 2, new byte[16])));
    }

    [Fact]
    public async Task Stop_WithoutStart_IsNoop()
    {
        using var encoder = new FfmpegEncoder(new StubLocator());
        await encoder.StopAsync();
        Assert.False(encoder.IsRunning);
    }

    [Fact]
    public async Task ProcessDeath_RaisesFailed()
    {
        var encoderProc = new FakeEncoderProcess();
        var factory = new FakeEncoderProcessFactory();
        factory.Return(new FakeEncoderProcess());
        factory.Return(encoderProc);

        using var encoder = new FfmpegEncoder(new StubLocator(), factory.Create);
        var failed = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
        encoder.ProcessFailed += (_, msg) => failed.TrySetResult(msg);

        await encoder.StartAsync(new EncoderOptions { RtmpUrl = "rtmp://x/y" });
        encoderProc.SignalExit(1);
        var msg = await failed.Task.WaitAsync(TimeSpan.FromSeconds(5));
        Assert.Contains("1", msg);
    }

    [Fact]
    public void Args_Gop_IsFpsTimesFour()
    {
        var options = new EncoderOptions { Fps = 60 };
        Assert.Equal(240, options.GopSize);

        var args = FfmpegArgs.Build(options, "libopenh264").ToArray();
        var g = Array.IndexOf(args, "-g");
        Assert.Equal("240", args[g + 1]);
        var keyint = Array.IndexOf(args, "-keyint_min");
        Assert.Equal("240", args[keyint + 1]);
    }

    [Fact]
    public void Args_IncludeInputOutputAndCompliance()
    {
        var args = FfmpegArgs.Build(
            new EncoderOptions
            {
                RtmpUrl = "rtmp://a.rtmp.youtube.com/live2/key",
                Width = 1920,
                Height = 1080,
                Fps = 60,
                BitrateKbps = 8000,
            },
            "libopenh264").ToArray();

        Assert.Contains("-f", args);
        Assert.Contains("rawvideo", args);
        Assert.Contains("pipe:0", args);
        Assert.Contains("1920x1080", args);
        Assert.Contains(@"\\.\pipe\ytllive_audio", args);
        Assert.Contains("f32le", args);
        Assert.Contains("-map", args);
        Assert.Contains("0:v", args);
        Assert.Contains("1:a", args);
        Assert.Contains("-sc_threshold", args);
        Assert.Contains("-bf", args);
        Assert.Contains("yuv420p", args);
        Assert.Contains("aac", args);
        Assert.Contains("flv", args);
        Assert.Contains("rtmp://a.rtmp.youtube.com/live2/key", args);
    }

    [Fact]
    public void ProgressParser_ParsesRealStatsLine()
    {
        var p = FfmpegProgressParser.TryParse(
            "frame=  123 fps= 59.9 q=28.0 size=   1024KiB time=00:00:02.04 bitrate= 4000.1kbits/s speed=1.00x");
        Assert.NotNull(p);
        Assert.Equal(123, p.Value.Frame);
        Assert.Equal(59.9, p.Value.Fps, 1);
        Assert.Equal(4000.1, p.Value.BitrateKbps, 1);
        Assert.Equal(2.04, p.Value.Duration.TotalSeconds, 2);
        Assert.Equal(1024 * 1024, p.Value.SizeBytes);
    }

    [Fact]
    public void ProgressParser_IgnoresBannerAndErrors()
    {
        Assert.Null(FfmpegProgressParser.TryParse("ffmpeg version 6.1 Copyright (c) 2000-2026 the FFmpeg developers"));
        Assert.Null(FfmpegProgressParser.TryParse("Error while opening encoder for output stream #0:0"));
        Assert.Null(FfmpegProgressParser.TryParse(""));
    }

    [Fact]
    public void EncoderPicker_PrefersHardware_NeverLibx264()
    {
        var listing =
            " V..... libx264   libx264 H.264 / AVC (codec h264)\n" +
            " V..... libopenh264  OpenH264 H.264 (codec h264)\n" +
            " V..... h264_nvenc  NVIDIA NVENC H.264 encoder (codec h264)\n";
        Assert.Equal("h264_nvenc", FfmpegEncoderPicker.Pick(listing));

        var onlyGpl = " V..... libx264   libx264 H.264 / AVC (codec h264)\n";
        Assert.Equal("libopenh264", FfmpegEncoderPicker.Pick(onlyGpl));

        var software = " V..... libopenh264  OpenH264 H.264 (codec h264)\n";
        Assert.Equal("libopenh264", FfmpegEncoderPicker.Pick(software));

        Assert.Equal("libopenh264", FfmpegEncoderPicker.Pick("no encoders at all"));
    }
}
