using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using ytLive.Services;
using ytLive.Services.Encoder;
namespace ytLive.Tests;
///
/// TASK 47 Good Dog: the alert clip decoder paces VIDEO to real time via the
/// frame-rate probe. The 2026-09-26 live failure was a healthy decoder whose
/// frames all dumped in the first ~1-2s (no probe = no pacing), leaving the box
/// frozen on the last frame while the audio ran its ~8s — read as "no video
/// plays". This drives the REAL against a fake
/// process + probe and records the pacing delay it requests per frame (the same
/// seam the media path's decoder test uses — the repo never runs a real codec).
///
public class AlertClipDecoderTests
{
[Fact]
public async Task AlertClipDecoder_PacesVideoFramesToTheProbedFrameRate()
{
const int w = 1, h = 1; // frame bytes = 4
var payload = new byte[12]; // three frames
var delays = new List();
using var decoder = new AlertClipDecoder(
"clip.mp4", w, h,
new FakeLocator(),
() => new FakeDecodeProcess(payload),
frameRateProbe: new FakeFrameRateProbe(100.0),
delay: (t, _) => { delays.Add(t); return Task.CompletedTask; });
var forwarded = 0;
var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
decoder.FrameAvailable += _ => forwarded++;
decoder.Completed += () => completed.TrySetResult(true);
decoder.Start();
await completed.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(3, forwarded);
// One pacing delay per emitted frame, each = 1/100s (the probed rate).
Assert.Equal(forwarded, delays.Count);
foreach (var d in delays)
Assert.True(Math.Abs((d - TimeSpan.FromMilliseconds(10)).TotalMilliseconds) < 0.001,
$"expected ~10ms, got {d.TotalMilliseconds}ms");
}
private sealed class FakeDecodeProcess : IDecodeProcess
{
private readonly MemoryStream _stream;
public FakeDecodeProcess(byte[] bytes) => _stream = new MemoryStream(bytes);
public void Start(ProcessStartInfo startInfo) { }
public Stream StandardOutput => _stream;
public bool HasExited => _stream.Position >= _stream.Length;
public int ExitCode => 0;
public void Kill() { }
public Task WaitForExitAsync(CancellationToken ct = default) => Task.CompletedTask;
public void Dispose() => _stream.Dispose();
}
private sealed class FakeFrameRateProbe : IFrameRateProbe
{
private readonly double _fps;
public FakeFrameRateProbe(double fps) => _fps = fps;
public Task ProbeAsync(string path, CancellationToken ct = default)
=> Task.FromResult(_fps);
}
private sealed class FakeLocator : IFfmpegLocator
{
public Task LocateAsync(CancellationToken ct = default) => Task.FromResult("ffmpeg.exe");
}
}