using ErsatzTV.Application.Playouts;
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Scheduling;
using NUnit.Framework;
using Shouldly;
using PlayoutsMapper = ErsatzTV.Application.Playouts.Mapper;
using SchedulingMapper = ErsatzTV.Application.Scheduling.Mapper;
namespace ErsatzTV.Tests.Application.Scheduling;
///
/// ersatztv#823. Guarding alone would have left the OTHER read
/// of the same six columns unguarded — the entity→view-model mappers, which feed
/// PlayoutController's response models and therefore the SPA.
///
/// Two things break without the guard, and neither is a C# exception, which is why the selector
/// tests cannot see them. web/src/screens/PlayoutScheduleEditors.tsx spreads the collection
/// (daysOfMonth: [...template.daysOfMonth]) and throws TypeError: not iterable on a
/// JSON null; and web/src/screens/playoutTemplateCalendar.ts's appliesToDate —
/// an exact TypeScript port of —
/// calls .includes on it.
///
///
/// So the mappers substitute the SAME unrestricted defaults the selector reads. That agreement is
/// the point: a DTO that said "empty" while the selector scheduled "unrestricted" would make the
/// preview calendar disagree with the playout it is previewing.
///
///
[TestFixture]
public class RecurrenceLimitsMapperNullTests
{
private static Template MinimalTemplate() =>
new()
{
Id = 7,
Name = "T",
TemplateGroupId = 1,
TemplateGroup = new TemplateGroup { Name = "G" },
Items = []
};
[Test]
public void ProgramScheduleAlternate_Null_Collections_Map_To_Unrestricted()
{
var alternate = new ProgramScheduleAlternate
{
Id = 1,
Index = 0,
ProgramScheduleId = 2,
DaysOfWeek = null!,
DaysOfMonth = null!,
MonthsOfYear = null!
};
PlayoutAlternateScheduleViewModel vm = PlayoutsMapper.ProjectToViewModel(alternate);
vm.DaysOfWeek.ShouldBe(AlternateScheduleSelector.AllDaysOfWeek());
vm.DaysOfMonth.ShouldBe(AlternateScheduleSelector.AllDaysOfMonth());
vm.MonthsOfYear.ShouldBe(AlternateScheduleSelector.AllMonthsOfYear());
// Never assigned back: these are single-column primitive collections, so writing the guard onto a
// tracked entity would persist the substituted set over the NULL
// (media.nullable-primitive-collection-mutation).
alternate.DaysOfWeek.ShouldBeNull();
alternate.DaysOfMonth.ShouldBeNull();
alternate.MonthsOfYear.ShouldBeNull();
}
[Test]
public void PlayoutTemplate_Null_Collections_Map_To_Unrestricted()
{
var template = new PlayoutTemplate
{
Id = 1,
Index = 0,
Template = MinimalTemplate(),
DecoTemplate = null,
DaysOfWeek = null!,
DaysOfMonth = null!,
MonthsOfYear = null!
};
PlayoutTemplateViewModel vm = SchedulingMapper.ProjectToViewModel(template);
vm.DaysOfWeek.ShouldBe(AlternateScheduleSelector.AllDaysOfWeek());
vm.DaysOfMonth.ShouldBe(AlternateScheduleSelector.AllDaysOfMonth());
vm.MonthsOfYear.ShouldBe(AlternateScheduleSelector.AllMonthsOfYear());
template.DaysOfWeek.ShouldBeNull();
template.DaysOfMonth.ShouldBeNull();
template.MonthsOfYear.ShouldBeNull();
}
///
/// An explicitly EMPTY collection is a recorded restriction of no days and must survive the mapper
/// unchanged. Without this, a guard written as "empty or null becomes All*" would pass the two tests
/// above while silently rewriting real user data on the way out.
///
/// There is one of these per MAPPER, not one in total. The two overloads are byte-identical
/// triples in different files, so a defensive edit to one alone is exactly the "one helper, two
/// callers" shape this repo has been bitten by: covering only the Playouts mapper would leave
/// the PlayoutTemplate one free to acquire an `empty-or-null` guard with the suite still green.
///
///
[Test]
public void An_Explicitly_Empty_Collection_Is_Not_Rewritten_By_The_PlayoutTemplate_Mapper()
{
var template = new PlayoutTemplate
{
Id = 1,
Index = 0,
Template = MinimalTemplate(),
DecoTemplate = null,
DaysOfWeek = [],
DaysOfMonth = [],
MonthsOfYear = []
};
PlayoutTemplateViewModel vm = SchedulingMapper.ProjectToViewModel(template);
vm.DaysOfWeek.ShouldBeEmpty();
vm.DaysOfMonth.ShouldBeEmpty();
vm.MonthsOfYear.ShouldBeEmpty();
}
///
/// The ProgramScheduleAlternate half of the same pair — see the PlayoutTemplate one above
/// for why there is one per MAPPER rather than one in total.
///
[Test]
public void An_Explicitly_Empty_Collection_Is_Not_Rewritten()
{
var alternate = new ProgramScheduleAlternate
{
Id = 1,
Index = 0,
ProgramScheduleId = 2,
DaysOfWeek = [],
DaysOfMonth = [],
MonthsOfYear = []
};
PlayoutAlternateScheduleViewModel vm = PlayoutsMapper.ProjectToViewModel(alternate);
vm.DaysOfWeek.ShouldBeEmpty();
vm.DaysOfMonth.ShouldBeEmpty();
vm.MonthsOfYear.ShouldBeEmpty();
}
}