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()
{
}
}
}