using System.Threading.Channels; using ErsatzTV.Application; using ErsatzTV.Application.ProgramSchedules; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Tests.Support; using LanguageExt; using NUnit.Framework; using Shouldly; using Unit = LanguageExt.Unit; namespace ErsatzTV.Tests.Application.ProgramSchedules; [TestFixture] public class ProgramScheduleHandlerTests { private InMemoryTvContext _db = null!; private ChannelWriter _worker = null!; [SetUp] public async Task SetUp() { _db = await InMemoryTvContext.CreateAsync(); _worker = System.Threading.Channels.Channel.CreateUnbounded().Writer; } [TearDown] public async Task TearDown() => await _db.DisposeAsync(); [Test] public async Task Update_Should_Return_NotFoundError_When_Schedule_Missing() { var handler = new UpdateProgramScheduleHandler(_db.Factory, _worker); Either result = await handler.Handle(MakeUpdate(999, "Missing"), CancellationToken.None); LeftOf(result).ShouldBeOfType(); } [Test] public async Task Delete_Should_Return_NotFoundError_When_Schedule_Missing() { var handler = new DeleteProgramScheduleHandler(_db.Factory); Either result = await handler.Handle(new DeleteProgramSchedule(999), CancellationToken.None); LeftOf(result).ShouldBeOfType(); } [Test] public async Task AddItem_Should_Return_NotFoundError_When_Schedule_Missing() { var handler = new AddProgramScheduleItemHandler(_db.Factory, _worker); Either result = await handler.Handle(MakeAdd(999, CollectionType.SearchQuery), CancellationToken.None); LeftOf(result).ShouldBeOfType(); } [Test] public async Task AddItem_Should_Return_ValidationError_When_Collection_Type_Is_Invalid() { int scheduleId = await SeedSchedule(); var handler = new AddProgramScheduleItemHandler(_db.Factory, _worker); Either result = await handler.Handle(MakeAdd(scheduleId, CollectionType.Collection), CancellationToken.None); BaseError error = LeftOf(result); error.ShouldNotBeOfType(); error.Value.ShouldContain("[Collection] is required"); } [Test] public async Task ReplaceItems_Should_Return_NotFoundError_When_Schedule_Missing() { var handler = new ReplaceProgramScheduleItemsHandler(_db.Factory, _worker); Either> result = await handler.Handle( new ReplaceProgramScheduleItems(999, [MakeReplace(0)]), CancellationToken.None); LeftOf(result).ShouldBeOfType(); } [Test] public async Task DeleteItem_Should_Return_NotFoundError_When_Item_Missing() { int scheduleId = await SeedSchedule(); var handler = new DeleteProgramScheduleItemHandler(_db.Factory, _worker); Either result = await handler.Handle(new DeleteProgramScheduleItem(scheduleId, 999), CancellationToken.None); LeftOf(result).ShouldBeOfType(); } private async Task SeedSchedule() { await using TvContext context = _db.CreateContext(); var schedule = new ProgramSchedule { Name = "Handlers", Items = [], Playouts = [], ProgramScheduleAlternates = [] }; context.ProgramSchedules.Add(schedule); await context.SaveChangesAsync(); return schedule.Id; } private static UpdateProgramSchedule MakeUpdate(int scheduleId, string name) => new( scheduleId, name, KeepMultiPartEpisodesTogether: true, TreatCollectionsAsShows: true, ShuffleScheduleItems: false, RandomStartPoint: false, FixedStartTimeBehavior.Flexible); private static AddProgramScheduleItem MakeAdd(int scheduleId, CollectionType collectionType) => new( scheduleId, StartType.Dynamic, StartTime: null, FixedStartTimeBehavior: null, PlayoutMode.One, collectionType, CollectionId: null, MultiCollectionId: null, SmartCollectionId: null, RerunCollectionId: null, MediaItemId: null, PlaylistId: null, SearchTitle: "News", SearchQuery: "news", PlaybackOrder.Shuffle, MarathonGroupBy.None, MarathonShuffleGroups: false, MarathonShuffleItems: false, MarathonBatchSize: null, FillWithGroupMode.None, MultipleMode.Count, MultipleCount: "1", PlayoutDuration: null, TailMode.None, DiscardToFillAttempts: null, CustomTitle: null, GuideMode.Normal, PreRollFillerId: null, MidRollFillerId: null, PostRollFillerId: null, TailFillerId: null, FallbackFillerId: null, WatermarkIds: [], GraphicsElementIds: [], PreferredAudioLanguageCode: null, PreferredAudioTitle: null, PreferredSubtitleLanguageCode: null, SubtitleMode: null); private static ReplaceProgramScheduleItem MakeReplace(int index) { AddProgramScheduleItem add = MakeAdd(1, CollectionType.SearchQuery); return new ReplaceProgramScheduleItem( index, add.StartType, add.StartTime, add.FixedStartTimeBehavior, add.PlayoutMode, add.CollectionType, add.CollectionId, add.MultiCollectionId, add.SmartCollectionId, add.RerunCollectionId, add.MediaItemId, add.PlaylistId, add.SearchTitle, add.SearchQuery, add.PlaybackOrder, add.MarathonGroupBy, add.MarathonShuffleGroups, add.MarathonShuffleItems, add.MarathonBatchSize, add.FillWithGroupMode, add.MultipleMode, add.MultipleCount, add.PlayoutDuration, add.TailMode, add.DiscardToFillAttempts, add.CustomTitle, add.GuideMode, add.PreRollFillerId, add.MidRollFillerId, add.PostRollFillerId, add.TailFillerId, add.FallbackFillerId, add.WatermarkIds, add.GraphicsElementIds, add.PreferredAudioLanguageCode, add.PreferredAudioTitle, add.PreferredSubtitleLanguageCode, add.SubtitleMode); } private static BaseError LeftOf(Either either) => either.Match(Left: e => e, Right: _ => throw new AssertionException("Expected a Left result")); }