using Nonemm.Core; namespace Nonemm.Contests.Tests; /// The figures the info window shows: rate bars, goals and the QSO timers. public class InfoWindowTests { private static readonly DateTime Now = new(2026, 5, 30, 12, 20, 0, DateTimeKind.Utc); private static readonly DateTime Start = new(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc); private static readonly TimeSpan Off = TimeSpan.FromMinutes(30); private static Qso MinutesAgo(int minutes, string computer = "A", int radio = 1) => TestLog.Contact("OM3XYZ") with { TimestampUtc = Now.AddMinutes(-minutes), StationName = computer, RadioNumber = radio, }; private static QsoTimerReading Read( IReadOnlyList log, QsoTimerOption option, DateTime? now = null) => QsoTimer.Read(log, option, Off, Start, now ?? Now, "A"); [Fact] public void HourlyBarsFollowTheClockAndScaleEachStretchToAnHour() { // 12:20 starts a 20-minute stretch: 12:20 is in the last bar, 12:19 and // 12:05 in the one before, and 11:59 in the one before that List log = [MinutesAgo(0), MinutesAgo(1), MinutesAgo(15), MinutesAgo(21)]; IReadOnlyList bars = RateBars.Hourly(log, Now, 20); Assert.Equal([0, 0, 3, 6, 3], bars); } [Fact] public void HourlyBarsLeaveOutContactsOlderThanTheGraph() { Assert.All(RateBars.Hourly([MinutesAgo(500)], Now, 20), bar => Assert.Equal(0, bar)); } [Fact] public void HourlyBarsLeaveOutContactsAfterNow() { Assert.All(RateBars.Hourly([MinutesAgo(-5)], Now, 60), bar => Assert.Equal(0, bar)); } [Fact] public void TheClockHourStartsAtTheTopOfTheHour() { List log = [MinutesAgo(5), MinutesAgo(19), MinutesAgo(21)]; Assert.Equal(2, RateBars.ThisClockHour(log, Now)); } [Fact] public void PointsAreSummedOverTheLastContacts() { List log = [.. Enumerable.Range(1, 5).Select(m => MinutesAgo(m) with { Points = 3 })]; Assert.Equal(9, RateBars.PointsOfLast(log, 3, Now, Off)); } [Fact] public void AGoalStaysInForceUntilTheNextOne() { Goals goals = Goals.Parse("100 60\n118 90\n"); Assert.Equal(60, goals.At(Start.AddHours(5), Start)); Assert.Equal(90, goals.At(Start.AddHours(6), Start)); } [Fact] public void GoalsAreFoundByContestDayAndClockHour() { Goals goals = Goals.Parse("112 80\n113 70\n201 20\n"); Assert.Equal(80, goals.At(Start.AddMinutes(20), Start)); Assert.Equal(70, goals.At(Start.AddHours(1), Start)); Assert.Equal(20, goals.At(Start.AddHours(13), Start)); } [Fact] public void BeforeTheStartTheGoalIsTheFirstHours() { Assert.Equal(80, Goals.Parse("112 80\n").At(Start.AddHours(-5), Start)); } [Fact] public void NinetySixHoursAfterTheStartTheGoalIsNothing() { Assert.Equal(0, Goals.Parse("112 80\n").At(Start.AddHours(96), Start)); } [Fact] public void WithNoGoalsTheGoalIsFifty() { Assert.Equal(50, Goals.Default.At(Now, Start)); Assert.Equal(50, Goals.Parse("# nothing here\n").At(Now, Start)); } [Fact] public void AHourOnItsOwnBelongsToTheDayBeingRead() { // 23 is day 1 and the 2 after it is the next day, because the key went back Goals goals = Goals.Parse("22 10\n23 20\n2 30\n"); Assert.Equal(30, goals.At(Start.AddHours(14), Start)); } [Fact] public void ACsvFileHasOneCommaPerLine() { Goals goals = Goals.Parse("112,45\n113,55\n", isCsv: true); Assert.Equal(55, goals.At(Start.AddHours(1), Start)); } [Fact] public void ATypeGoalHeaderIsSkipped() { Goals goals = Goals.Parse("Type=GOAL SubType=\n112 45\n"); Assert.Equal(45, goals.At(Start, Start)); } [Fact] public void AStatisticsFileTakesTheTotalColumn() { string file = "Day Hour 160 80 40 20 15 10 Total\n20260530 12 0 0 5 20 1 0 26\n20260530 13 0 0 4 30 2 0 36\n"; Goals goals = Goals.Parse(file); Assert.Equal(36, goals.At(Start.AddHours(1), Start)); } [Fact] public void AStatisticsFileCanTakeOneBandsColumn() { string file = "Day Hour 160 80 40 20 15 10 Total\n20260530 12 0 0 5 20 1 0 26\n"; // 3 back from the total is 20 m Assert.Equal(20, Goals.Parse(file, fromEnd: 3).At(Start, Start)); } [Fact] public void GoalsWrittenOutCanBeReadBack() { Goals goals = Goals.Parse("112 80\n201 40\n"); Assert.Equal("Type=GOAL SubType=\n112 80\n201 40\n", Goals.Parse(goals.ToText()).ToText()); } [Fact] public void TextWithNoGoalInItIsAnError() { Assert.Throws(() => Goals.Parse("abc\ndef\n")); } [Fact] public void TheCountUpTimerCountsFromTheMinuteAfterTheLastContact() { QsoTimerReading reading = Read([MinutesAgo(5)], QsoTimerOption.CountUp); // 12:15, so the count starts at 12:16 and now is 12:20 Assert.Equal("00:04", reading.Text); Assert.Equal(TimerState.Plain, reading.State); } [Fact] public void TheCountUpTimerIsMetAtTheMinimumOffTime() { Assert.Equal(TimerState.Met, Read([MinutesAgo(32)], QsoTimerOption.CountUp).State); } [Fact] public void TheCountDownTimerShowsWhatIsLeft() { Qso last = TestLog.Contact("OM3XYZ") with { TimestampUtc = Now.AddMinutes(-10).AddSeconds(40) }; QsoTimerReading reading = Read([last], QsoTimerOption.CountDown); // the contact was at 12:10:40, so the count starts at 12:11 and 9 minutes have gone Assert.Equal("00:21", reading.Text); } [Fact] public void TheCountDownTimerIsMetAtZero() { QsoTimerReading reading = Read([MinutesAgo(40)], QsoTimerOption.CountDown); Assert.Equal("00:00", reading.Text); Assert.Equal(TimerState.Met, reading.State); } [Fact] public void TheCumulativeTimerCountsOnlyBreaksOfTheMinimumOrMore() { // breaks of 100 and 60 minutes count, and the gap of 10 minutes does not List log = [MinutesAgo(220), MinutesAgo(120), MinutesAgo(60), MinutesAgo(50)]; // 08:40 to 10:20 leaves 99 whole minutes and 10:20 to 11:20 leaves 59 QsoTimerReading reading = QsoTimer.Read( log, QsoTimerOption.Cumulative, Off, Now.AddHours(-6), Now, "A"); Assert.Equal("02:38", reading.Text); } [Fact] public void TimeSinceTheLastContactIsShownInMinutesAndSeconds() { QsoTimerReading reading = Read([MinutesAgo(2)], QsoTimerOption.AllStations); Assert.Equal("02:00", reading.Text); Assert.Equal(TimerState.Warning, reading.State); } [Fact] public void TimeSinceTheLastContactTurnsRedAfterThreeMinutes() { Assert.Equal(TimerState.Over, Read([MinutesAgo(4)], QsoTimerOption.AllStations).State); } [Fact] public void AnHourSinceTheLastContactAddsTheHours() { Assert.Equal("01:05:00", Read([MinutesAgo(65)], QsoTimerOption.AllStations).Text); } [Fact] public void ADayWithoutAContactIsSaidSo() { Assert.Equal("> 24hrs", Read([MinutesAgo(1500)], QsoTimerOption.AllStations).Text); } [Fact] public void ThisStationLeavesOutContactsFromOtherComputers() { List log = [MinutesAgo(1, computer: "B"), MinutesAgo(10)]; Assert.Equal("10:00", Read(log, QsoTimerOption.ThisStation).Text); } [Fact] public void TheRightRadioLooksOnlyAtRadioTwo() { List log = [MinutesAgo(1, radio: 1), MinutesAgo(7, radio: 2)]; Assert.Equal("07:00", Read(log, QsoTimerOption.RightRadio).Text); } [Fact] public void ALogWithNoContactsShowsNoTime() { Assert.Equal("0:00", Read([], QsoTimerOption.AllStations).Text); } [Fact] public void TheBoxIsLabelledForTheRadiosTheStationHas() { Assert.Equal("Both Radios", QsoTimer.Caption(QsoTimerOption.ThisStation, 30, isSo2r: true)); Assert.Equal("VFO B", QsoTimer.Caption(QsoTimerOption.RightRadio, 30, isSo2r: false)); Assert.Equal("Off Time v (30)", QsoTimer.Caption(QsoTimerOption.CountDown, 30, isSo2r: false)); } }