using AmiReel.Models; using AmiReel.Services; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace AmiReel.Tests.Services; [TestClass] public class RenderPipelineTests { private readonly List _tempDirectories = []; [TestCleanup] public void Cleanup() { foreach (string directory in _tempDirectories) { try { Directory.Delete(directory, true); } catch { } } } private string CreateTempDirectory() { string directory = Path.Combine(Path.GetTempPath(), "AmiReelTests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(directory); _tempDirectories.Add(directory); return directory; } private static RenderSettings ValidSettings(IReadOnlyList inputFiles, string outputDirectory, string endCardPath = "") => new() { InputFiles = inputFiles, EndCardPath = endCardPath, OutputDirectory = outputDirectory, OutputName = "output", FfmpegPath = "", FfprobePath = "", Encoder = EncoderMode.Auto, TrimStart = 0, FadeSeconds = 1, EndCardHoldSeconds = 1, ThumbnailInterval = 10, }; [TestMethod] public void Validate_WithNoInputFiles_ThrowsArgumentException() { // Arrange RenderSettings settings = ValidSettings([], @"C:\out"); // Act & Assert Assert.ThrowsExactly(() => RenderPipeline.Validate(settings)); } [TestMethod] public void Validate_WithMissingInputFile_ThrowsFileNotFoundException() { // Arrange string directory = CreateTempDirectory(); RenderSettings settings = ValidSettings([Path.Combine(directory, "missing.avi")], directory); // Act & Assert Assert.ThrowsExactly(() => RenderPipeline.Validate(settings)); } [TestMethod] public void Validate_WithMissingEndCard_ThrowsFileNotFoundException() { // Arrange string directory = CreateTempDirectory(); string input = Path.Combine(directory, "clip.avi"); File.WriteAllText(input, "data"); RenderSettings settings = ValidSettings([input], directory, endCardPath: Path.Combine(directory, "missing.png")); // Act & Assert Assert.ThrowsExactly(() => RenderPipeline.Validate(settings)); } [TestMethod] public void Validate_WithBlankOutputDirectory_ThrowsArgumentException() { // Arrange string directory = CreateTempDirectory(); string input = Path.Combine(directory, "clip.avi"); File.WriteAllText(input, "data"); RenderSettings settings = ValidSettings([input], ""); // Act & Assert Assert.ThrowsExactly(() => RenderPipeline.Validate(settings)); } [TestMethod] public void Validate_WithInvalidOutputName_ThrowsArgumentException() { // Arrange string directory = CreateTempDirectory(); string input = Path.Combine(directory, "clip.avi"); File.WriteAllText(input, "data"); RenderSettings settings = ValidSettings([input], directory) with { OutputName = "bad" + Path.GetInvalidFileNameChars()[0] }; // Act & Assert Assert.ThrowsExactly(() => RenderPipeline.Validate(settings)); } [TestMethod] public void Validate_WithThumbnailIntervalLessThanOne_ThrowsArgumentException() { // Arrange string directory = CreateTempDirectory(); string input = Path.Combine(directory, "clip.avi"); File.WriteAllText(input, "data"); RenderSettings settings = ValidSettings([input], directory) with { ThumbnailInterval = 0 }; // Act & Assert Assert.ThrowsExactly(() => RenderPipeline.Validate(settings)); } [TestMethod] public void Validate_WithAllRequirementsMet_DoesNotThrow() { // Arrange string directory = CreateTempDirectory(); string input = Path.Combine(directory, "clip.avi"); File.WriteAllText(input, "data"); RenderSettings settings = ValidSettings([input], directory); // Act & Assert (no exception) RenderPipeline.Validate(settings); } [TestMethod] public void BuildProgressMessage_WithOnlyTime_ReturnsTimeRangeOnly() { // Act string message = RenderPipeline.BuildProgressMessage(TimeSpan.FromSeconds(30), 60, null, null, null, 50); // Assert Assert.AreEqual("00:00:30 / 00:01:00", message); } [TestMethod] public void BuildProgressMessage_WithAllOptionalFields_IncludesFpsSpeedAndFrame() { // Arrange: numbers render using the current culture (this is user-facing text), // so build the expected fragments the same way rather than hard-coding "." / ",". const double fps = 49.8; const double speed = 0.99; const int frame = 1500; const int totalFrames = 3000; // ceil(60s * 50fps) // Act string message = RenderPipeline.BuildProgressMessage(TimeSpan.FromSeconds(30), 60, fps, speed, frame, 50); // Assert StringAssert.Contains(message, "00:00:30 / 00:01:00"); StringAssert.Contains(message, $"{fps:0.#} fps"); StringAssert.Contains(message, $"{speed:0.##}x"); StringAssert.Contains(message, $"frame {frame:N0} / {totalFrames:N0}"); } [TestMethod] public void MoveSourceVideos_WithNewDestination_MovesFileIntoOriginalsSubfolder() { // Arrange string outputDirectory = CreateTempDirectory(); string sourceDirectory = CreateTempDirectory(); string source = Path.Combine(sourceDirectory, "clip.avi"); File.WriteAllText(source, "data"); List logs = []; // Act IReadOnlyList result = RenderPipeline.MoveSourceVideos([source], outputDirectory, logs.Add); // Assert string expected = Path.Combine(outputDirectory, "originals", "clip.avi"); Assert.AreEqual(expected, result.Single()); Assert.IsTrue(File.Exists(expected)); Assert.IsFalse(File.Exists(source)); } [TestMethod] public void MoveSourceVideos_WithDuplicateFilename_AppendsNumericSuffix() { // Arrange string outputDirectory = CreateTempDirectory(); string sourceDirectory = CreateTempDirectory(); string sourceA = Path.Combine(sourceDirectory, "a", "clip.avi"); string sourceB = Path.Combine(sourceDirectory, "b", "clip.avi"); Directory.CreateDirectory(Path.GetDirectoryName(sourceA)!); Directory.CreateDirectory(Path.GetDirectoryName(sourceB)!); File.WriteAllText(sourceA, "data-a"); File.WriteAllText(sourceB, "data-b"); // Act IReadOnlyList result = RenderPipeline.MoveSourceVideos([sourceA, sourceB], outputDirectory, _ => { }); // Assert Assert.AreEqual(2, result.Distinct(StringComparer.OrdinalIgnoreCase).Count()); Assert.IsTrue(result.All(File.Exists)); } [TestMethod] public void MoveSourceVideos_WithMissingSourceFile_LogsWarningAndKeepsOriginalPath() { // Arrange string outputDirectory = CreateTempDirectory(); string missingSource = Path.Combine(CreateTempDirectory(), "gone.avi"); List logs = []; // Act IReadOnlyList result = RenderPipeline.MoveSourceVideos([missingSource], outputDirectory, logs.Add); // Assert Assert.AreEqual(missingSource, result.Single()); Assert.IsTrue(logs.Any(line => line.Contains("no longer exists", StringComparison.OrdinalIgnoreCase))); } }