using LiveRecorder.Domain.Entities;
using LiveRecorder.Domain.Enums;
using LiveRecorder.Infrastructure.Services;
using System.Xml.Linq;
namespace LiveRecorder.Tests;
public sealed class RecordingContinuityTests
{
[Theory]
[InlineData(true, false, true)]
[InlineData(false, true, true)]
[InlineData(false, false, false)]
public void SessionTransition_IsTreatedAsActive(bool process, bool transition, bool expected) =>
Assert.Equal(expected, FfmpegService.IsSessionRuntimeActive(process, transition));
[Theory]
[InlineData(1, 5)]
[InlineData(2, 15)]
[InlineData(3, 30)]
[InlineData(4, 60)]
[InlineData(5, 120)]
[InlineData(50, 120)]
public void RuntimeRecoveryBackoff_IsBoundedAtTwoMinutes(int attempt, int seconds) =>
Assert.Equal(TimeSpan.FromSeconds(seconds), FfmpegService.GetRuntimeRecoveryDelay(attempt));
[Theory]
[InlineData(null, false, false)]
[InlineData(0.0, false, false)]
[InlineData(0.1, true, false)]
[InlineData(4.99, true, false)]
[InlineData(5.0, true, true)]
public void MediaDuration_SeparatesReadableFromStandalone(double? duration, bool readable, bool standalone)
{
Assert.Equal(readable, FfmpegService.IsReadableMediaDuration(duration));
Assert.Equal(standalone, FfmpegService.IsStandaloneMediaDuration(duration));
}
[Fact]
public void OfflineSample_RequiresConfirmation_ButLiveDoesNot()
{
Assert.True(LiveRoomPollingBackgroundService.RequiresOfflineConfirmation(false));
Assert.False(LiveRoomPollingBackgroundService.RequiresOfflineConfirmation(true));
}
[Theory]
[InlineData(false, RecordTaskStatus.Starting, true)]
[InlineData(false, RecordTaskStatus.Running, true)]
[InlineData(true, RecordTaskStatus.Running, false)]
[InlineData(false, RecordTaskStatus.Completed, false)]
public void ArtifactlessRecovery_ReusesOnlyActiveTask(bool hasMedia, RecordTaskStatus status, bool expected) =>
Assert.Equal(expected, FfmpegService.ShouldReuseRecoveryTask(hasMedia, status));
[Fact]
public void SessionRecoveryMarker_KeepsSessionActive()
{
var session = new RecordSession(Guid.NewGuid(), "origin", RecordOutputFormat.Mp4, RecordSaveMode.Segmented, DateTimeOffset.UtcNow);
session.MarkRecovering("[runtime-recovery] attempt=4; curl EOF", DateTimeOffset.UtcNow);
Assert.Equal(RecordSessionStatus.Starting, session.Status);
Assert.StartsWith("[runtime-recovery]", session.ErrorMessage);
Assert.Null(session.EndedAt);
}
[Fact]
public void MergedSource_PointsAtTargetAndBecomesHidden()
{
var source = new RecordTask(Guid.NewGuid(), Guid.NewGuid(), 2, "origin", RecordOutputFormat.Mp4, DateTimeOffset.UtcNow);
var targetId = Guid.NewGuid();
source.MarkMergedSource(targetId, DateTimeOffset.UtcNow);
Assert.True(source.IsHiddenArtifactSource);
Assert.Equal(targetId, source.MergedIntoRecordTaskId);
}
[Fact]
public void MergeWindow_FirstShortFragment_MergesForward()
{
var window = ShortFragmentConsolidationService.ResolveMergeWindow([1.2, 60], 0);
Assert.Equal((0, 1), window);
}
[Fact]
public void MergeWindow_TrailingShortFragment_MergesBackward()
{
var window = ShortFragmentConsolidationService.ResolveMergeWindow([60, 1.2], 0);
Assert.Equal((0, 1), window);
}
[Fact]
public void MergeWindow_ConsecutiveShortFragments_AreAccumulated()
{
var window = ShortFragmentConsolidationService.ResolveMergeWindow([1.2, 2.4, 60], 1);
Assert.Equal((0, 2), window);
}
[Fact]
public void DanmakuMerge_ShiftsRelativeOffset_ButPreservesAbsoluteTimestamp()
{
var chat = XElement.Parse("hi");
var shiftedChat = ShortFragmentConsolidationService.AdjustDanmakuElementOffsets(chat, 4.25);
Assert.StartsWith("5.8,", shiftedChat.Attribute("p")!.Value);
Assert.Contains("1720000000000", shiftedChat.Attribute("p")!.Value);
var eventElement = XElement.Parse("");
var shiftedEvent = ShortFragmentConsolidationService.AdjustDanmakuElementOffsets(eventElement, 4.25);
Assert.Equal("6.2", shiftedEvent.Attribute("offset")!.Value);
Assert.Equal("1720000000123", shiftedEvent.Attribute("ts")!.Value);
}
}