using Nonemm.Contests.Rules; using Nonemm.Core; using Nonemm.Core.Country; namespace Nonemm.Session.Tests; public class QsoEditorTests { private const string Countries = """ Germany: 14: 28: EU: 51.00: -10.00: -1.0: DL: DL,DK,DJ; Japan: 25: 45: AS: 36.40: -138.38: -9.0: JA: JA,JH,JR; """; private static readonly StationInfo Me = new() { Callsign = "DL1ABC", CqZone = 14, ItuZone = 28, Continent = "EU", CountryPrefix = "DL", }; private static QsoEditor CqWw() => new(new CqWorldWide(ModeCategory.Cw), Me, CountryFile.Parse(Countries)); private static QsoEditor Sweeps() => new(new Sweepstakes(ModeCategory.Cw), Me, CountryFile.Parse(Countries)); private static Qso Contact() => new() { Id = "1", TimestampUtc = new DateTime(2026, 5, 30, 12, 34, 56, DateTimeKind.Utc), Call = Callsign.Parse("JA1XYZ"), Frequency = Frequency.FromKilohertz(14_025), Mode = Modes.Cw, ContestName = "CQWW", SentReport = "599", ReceivedReport = "599", Zone = 25, CountryPrefix = "JA", Continent = "AS", }; [Fact] public void TheColumnsFollowTheContestExchange() { IReadOnlyList fields = [.. CqWw().Columns.Select(c => c.Field)]; Assert.Contains(QsoField.Zone, fields); Assert.DoesNotContain(QsoField.Section, fields); Assert.DoesNotContain(QsoField.SentNumber, fields); } /// Sweepstakes counts serial numbers, so the log shows the one we sent. [Fact] public void AContestWithSerialNumbersGetsASentNumberColumn() => Assert.Contains(QsoField.SentNumber, Sweeps().Columns.Select(c => c.Field)); [Fact] public void ChangingTheCallLooksTheCountryUpAgain() { QsoEdit edit = CqWw().Apply(Contact(), QsoField.Call, "dl2xyz"); Assert.True(edit.IsAccepted); Assert.Equal("DL2XYZ", edit.Result!.Call.Text); Assert.Equal("DL", edit.Result.CountryPrefix); Assert.Equal("EU", edit.Result.Continent); Assert.Equal("DL2", edit.Result.WpxPrefix); } [Theory] [InlineData("12345")] [InlineData("AB")] [InlineData("JA1XY Z")] public void ATextThatIsNotShapedLikeACallIsRefused(string text) { QsoEdit edit = CqWw().Apply(Contact(), QsoField.Call, text); Assert.False(edit.IsAccepted); Assert.Contains("callsign", edit.Error); } [Fact] public void ARefusedEditReturnsNoContact() => Assert.Null(CqWw().Apply(Contact(), QsoField.Zone, "41").Result); [Theory] [InlineData("40", true)] [InlineData("1", true)] [InlineData("41", false)] [InlineData("0", false)] [InlineData("two", false)] public void CqZonesRunFromOneToForty(string text, bool accepted) => Assert.Equal(accepted, CqWw().Apply(Contact(), QsoField.Zone, text).IsAccepted); [Fact] public void AnUnknownSectionIsRefused() => Assert.False(Sweeps().Apply(Contact(), QsoField.Section, "ZZ").IsAccepted); [Fact] public void ASectionIsStoredUpperCase() => Assert.Equal("STX", Sweeps().Apply(Contact(), QsoField.Section, "stx").Result!.Section); [Fact] public void ACommentKeepsTheCaseItWasTypedIn() => Assert.Equal("Nice signal", CqWw().Apply(Contact(), QsoField.Comment, "Nice signal").Result!.Comment); [Fact] public void ClearingAFieldIsAllowed() => Assert.Equal(0, CqWw().Apply(Contact(), QsoField.Zone, "").Result!.Zone); [Fact] public void TheTimeIsReadAsUtc() { QsoEdit edit = CqWw().Apply(Contact(), QsoField.Time, "2026-05-30 13:00:00"); Assert.Equal(new DateTime(2026, 5, 30, 13, 0, 0, DateTimeKind.Utc), edit.Result!.TimestampUtc); } [Fact] public void AFrequencyIsTypedInKilohertz() => Assert.Equal( 21_005_000, CqWw().Apply(Contact(), QsoField.Frequency, "21005").Result!.Frequency.Hertz); [Fact] public void AModeTheLoggerDoesNotKnowIsRefused() => Assert.False(CqWw().Apply(Contact(), QsoField.Mode, "SSTV").IsAccepted); [Fact] public void AnEmptyNumberReadsBackAsAnEmptyColumn() => Assert.Equal("", QsoEditor.Read(Contact() with { Zone = 0 }, QsoField.Zone)); }