using TeleWave.Application.Programming.Planning; using TeleWave.Application.Programming.Templates; using TeleWave.Domain.Programming; using Xunit; namespace TeleWave.Application.Tests.Programming; /// /// Применимость слоёв (см. 3.4) и разрешение перекрытий: какие слоты реально действуют в сутки. /// public class LayerApplicabilityTests { private static readonly TimeOnly DayStart = new(6, 0); [Fact] public void Empty_CoversEveryDate() { var applicability = new LayerApplicability(); Assert.True(applicability.IsEmpty); Assert.True(applicability.Covers(new DateOnly(2026, 3, 17))); } [Theory] [InlineData(2026, 12, 25, true)] // внутри, до Нового года [InlineData(2026, 1, 3, true)] // внутри, после Нового года [InlineData(2026, 12, 20, true)] // ровно начало [InlineData(2026, 1, 8, true)] // ровно конец [InlineData(2026, 12, 19, false)] // за день до начала [InlineData(2026, 1, 9, false)] // на следующий день после конца [InlineData(2026, 6, 15, false)] // середина года public void AnnualRange_CrossingNewYear_IsInclusiveOnBothEnds( int year, int month, int day, bool expected ) { // «20 декабря — 8 января» задаётся один раз и работает в любом году, поэтому сравнение идёт // по паре (месяц, день), а не по датам. var applicability = new LayerApplicability(AnnualRanges: [new AnnualRange(12, 20, 1, 8)]); Assert.Equal(expected, applicability.Covers(new DateOnly(year, month, day))); } [Fact] public void AnnualRange_WithinOneYear_DoesNotWrap() { var applicability = new LayerApplicability(AnnualRanges: [new AnnualRange(6, 1, 8, 31)]); Assert.True(applicability.Covers(new DateOnly(2026, 7, 4))); Assert.False(applicability.Covers(new DateOnly(2026, 1, 4))); } [Fact] public void Sections_AreCombinedWithOr() { // Понедельник ИЛИ конкретная дата: суббота из списка дат проходит, обычная суббота — нет. var applicability = new LayerApplicability( Weekdays: [1], SpecificDates: [new DateOnly(2026, 3, 21)] ); Assert.True(applicability.Covers(new DateOnly(2026, 3, 16))); // понедельник Assert.True(applicability.Covers(new DateOnly(2026, 3, 21))); // суббота из списка Assert.False(applicability.Covers(new DateOnly(2026, 3, 14))); // другая суббота } [Fact] public void Build_SkipsSlotsCoveredByHigherPriorityLayer() { // Слот младшего слоя пропускается целиком, а не обрезается: половина слота означала бы // половину настройки — своей группы и стратегии у половинки нет. var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка"); var top = template.AddLayer("Прайм", 100); var bottom = template.AddLayer("Обычный", 50); AddSlot(top, "Кино", new TimeOnly(20, 0), 120); AddSlot(bottom, "Сериал", new TimeOnly(20, 30), 60); AddSlot(bottom, "Ночь", new TimeOnly(23, 0), 60); var titles = Build(template).Select(s => s.Slot.Title).Distinct().ToList(); Assert.Contains("Кино", titles); Assert.Contains("Ночь", titles); Assert.DoesNotContain("Сериал", titles); } [Fact] public void Build_DropsLayersThatDoNotApplyOnTheDate() { var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка"); var newYear = template.AddLayer("Новогодний", 100); newYear.Update( newYear.Name, newYear.Priority, new LayerApplicability(AnnualRanges: [new AnnualRange(12, 20, 1, 8)]).ToJson(), isEnabled: true ); AddSlot(newYear, "Ирония судьбы", new TimeOnly(20, 0), 180); var usual = template.AddLayer("Обычный", 50); AddSlot(usual, "Вечерний сериал", new TimeOnly(20, 0), 60); var march = Build(template, new DateTimeOffset(2026, 3, 17, 12, 0, 0, TimeSpan.Zero)) .Select(s => s.Slot.Title) .ToList(); var december = Build(template, new DateTimeOffset(2026, 12, 25, 12, 0, 0, TimeSpan.Zero)) .Select(s => s.Slot.Title) .ToList(); Assert.Contains("Вечерний сериал", march); Assert.DoesNotContain("Ирония судьбы", march); Assert.Contains("Ирония судьбы", december); Assert.DoesNotContain("Вечерний сериал", december); } [Fact] public void Build_DisabledLayerIsIgnored() { var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка"); var layer = template.AddLayer("Выключённый", 100); AddSlot(layer, "Ничего", new TimeOnly(20, 0), 60); layer.Update(layer.Name, layer.Priority, null, isEnabled: false); Assert.Empty(Build(template)); } private static void AddSlot( GridLayer layer, string title, TimeOnly start, int durationMinutes ) => layer.AddSlot(title, start, durationMinutes); private static IReadOnlyList Build( ScheduleTemplate template, DateTimeOffset? from = null ) { var start = from ?? new DateTimeOffset(2026, 3, 17, 12, 0, 0, TimeSpan.Zero); return EffectiveGridBuilder.Build( template, utcOffsetMinutes: 180, DayStart, start, start.AddHours(18) ); } }