using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Sqlite; using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.ViewModels; namespace ytLive.Tests; /// /// TASK 21 Good Dog (2026-11-05): a MediaSource layer exists but nothing ever started /// a decode session, so a video file layer rendered nothing in a running app. This /// drives the real add path through and asserts the piece the /// unit adds — the session lifecycle: adding a video file starts exactly one decoder, /// the decoded frames reach the layer, a loop flip reaches that decoder, and removing /// the layer stops it. /// [Collection("RealApp")] public sealed class MediaSourceAcquisitionTests { private readonly RealAppHost _app; public MediaSourceAcquisitionTests(RealAppHost app) => _app = app; [Fact] public void AddingAVideoLayer_StartsTheDecoder_AndRemovingItStopsTheDecoder() { _app.Run(() => { var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db"); var videoPath = Path.Combine(Path.GetTempPath(), "starting-soon.mp4"); var decoders = new List(); MainViewModel.LayoutPathOverride = tempDb; MainViewModel.MediaFrameSourceFactoryOverride = key => { var decoder = new FakeMediaFrameSource(key); decoders.Add(decoder); return decoder; }; var window = new MainWindow(); var vm = (MainViewModel)window.DataContext; try { window.Show(); window.UpdateLayout(); var scene = vm.StagedScene!; vm.AddMediaSource(videoPath); var layer = scene.Elements.OfType().Single(s => s.IsMediaSource); Assert.Equal(videoPath, layer.MediaPath); Assert.Equal(SourceType.MediaSource, layer.Type); Assert.Single(decoders); var decoder = decoders[0]; Assert.Equal(videoPath, decoder.Key); Assert.Equal(1, decoder.StartCount); // A decoded frame reaches the layer the preview/renderer draws from — // otherwise the session would be running and still invisible. Assert.Null(layer.DisplaySource); decoder.Emit(new VideoFrame(64, 36, new byte[64 * 36 * 4])); Assert.NotNull(layer.DisplaySource); // Loop intent is per Source; the shared session is the per-path answer. Assert.False(decoder.Looping); layer.MediaIsLooping = true; Assert.True(decoder.Looping); vm.RemoveSourceCommand.Execute(layer); Assert.DoesNotContain(layer, scene.Elements); Assert.Equal(1, decoder.StopCount); } finally { window.Close(); MainViewModel.MediaFrameSourceFactoryOverride = null; MainViewModel.LayoutPathOverride = null; SqliteConnection.ClearAllPools(); try { File.Delete(tempDb); } catch { /* best-effort cleanup */ } } }); } /// Stands in for the ffmpeg decode process (the manager's factory seam). private sealed class FakeMediaFrameSource : IMediaFrameSource { public FakeMediaFrameSource(string key) => Key = key; public string Key { get; } public bool Looping { get; set; } public int StartCount { get; private set; } public int StopCount { get; private set; } public event Action? FrameAvailable; public event Action? AudioAvailable { add { } remove { } } public event Action? Completed { add { } remove { } } public Task StartAsync(CancellationToken cancellationToken = default) { StartCount++; return Task.CompletedTask; } public Task StopAsync() { StopCount++; return Task.CompletedTask; } public void Emit(VideoFrame frame) => FrameAvailable?.Invoke(frame); public void Dispose() { } } }