using System;
using System.Diagnostics;
using Xunit;
using ytLive.Helpers;
namespace ytLive.Tests;
///
/// UNIT 0 (2026-10-05): the camera and the pump could both drop frames invisibly —
/// a 60fps rawvideo container stamps frames by ARRIVAL, so a missing camera frame
/// and a padded repeat look identical in the recorded file, and the old stats line
/// counted SUBMITTED frames (always the full padded target) over an unknown number
/// of renders. These tests pin the one shared counter that makes the two measurable:
/// gaps, hitches, and the longest stall in a window of a timestamped stream.
///
public sealed class FrameCadenceTests
{
private static long Ms(double ms) => (long)(ms * Stopwatch.Frequency / 1000d);
private static long AtMs(double ms) => Ms(ms);
[Fact]
public void SteadyStream_ReportsNominalRateAndNoHitches()
{
var cadence = new FrameCadence();
// 30 frames at a true 33.3ms period: 29 gaps, none over the 50ms threshold.
for (var i = 0; i < 30; i++) cadence.Add(AtMs(i * (1000d / 30d)));
var s = cadence.Summarize();
Assert.Equal(30, s.Frames);
Assert.Equal(29, s.Gaps);
Assert.Equal(0, s.Hitches);
Assert.Equal(30d, Math.Round(s.RatePerSecond, 0));
Assert.Equal(33.3, Math.Round(s.AverageGapMs, 1));
Assert.InRange(s.LongestGapMs, 33.0, 33.5);
}
[Fact]
public void DroppedFrames_CountAsHitchesAndLongestGap()
{
var cadence = new FrameCadence();
// 30fps with two dropped frames (66.6ms gaps) and one double-frame (0.1ms):
// the pattern the 2026-10-05 recording showed in the webcam corner.
foreach (var ms in new[]
{
0.0, 33.3, 33.4, 66.7, 100.0, 133.3, 199.9, 233.2, 266.5, 333.1, 366.4,
})
cadence.Add(AtMs(ms));
var s = cadence.Summarize();
Assert.Equal(11, s.Frames);
Assert.Equal(10, s.Gaps); // the bunched pair still counts as an interval
Assert.Equal(2, s.Hitches); // the two dropped-frame gaps at 30fps
Assert.Equal(66.6, Math.Round(s.LongestGapMs, 1));
}
[Fact]
public void OutOfOrderStamp_CountsAsFrameWithoutPoisoningGaps()
{
var cadence = new FrameCadence();
cadence.Add(AtMs(0));
cadence.Add(AtMs(33.3));
cadence.Add(AtMs(10.0)); // out of order — a stamp from another clock domain
cadence.Add(AtMs(66.6));
var s = cadence.Summarize();
Assert.Equal(4, s.Frames);
Assert.Equal(2, s.Gaps); // the two valid intervals only
Assert.Equal(0, s.Hitches);
Assert.Equal(33.3, Math.Round(s.LongestGapMs, 1));
}
[Fact]
public void Reset_StartsFreshWindowAndKeepsThreshold()
{
var cadence = new FrameCadence(hitchThresholdMs: 20);
cadence.Add(AtMs(0));
cadence.Add(AtMs(100));
Assert.Equal(1, cadence.Hitches);
cadence.Reset();
Assert.Equal(0, cadence.Frames);
Assert.Equal(0, cadence.Gaps);
Assert.Equal(20, cadence.Summarize().HitchThresholdMs);
Assert.Contains("no frames", cadence.Summarize().Describe("cam"));
}
[Fact]
public void Describe_NamesTheWindowRateHitchesAndLongestGap()
{
var cadence = new FrameCadence();
foreach (var ms in new[] { 0.0, 16.7, 33.4, 50.1, 120.0 }) cadence.Add(AtMs(ms));
Assert.Equal(
"webcam 5f/0.1s 33.3/s, 4 gaps, 1 >= 50ms, avg 30.0ms, longest 69.9ms",
cadence.Summarize().Describe("webcam"));
}
[Fact]
public void ThresholdsMustBePositive()
{
Assert.Throws(() => new FrameCadence(0));
Assert.Throws(() => new FrameCadence(-1));
}
}