using Nonemm.Contests; using Nonemm.Contests.Rules; using Nonemm.Core; using Nonemm.Core.Calls; using Nonemm.Core.Country; using Nonemm.Storage; namespace Nonemm.Session.Tests; public class RadioPositionTests { 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 RadioPosition Session(Contest? contest = null) => new(Shared(new FakeLogStore(), contest)); private static ContestSession Shared(FakeLogStore store, Contest? contest = null) { ContestInstance instance = store.AddContest(new ContestInstance { ContestNumber = 0, ContestName = "CQWW", }); return new ContestSession( store, contest ?? new CqWorldWide(ModeCategory.Cw), instance, Me, CountryFile.Parse(Countries)); } private static void Type(RadioPosition session, string call, string zone) { session.Entry.Call = call; session.Entry.Set(ExchangeSlot.ReceivedReport, "599"); session.Entry.Set(ExchangeSlot.Zone, zone); } [Fact] public void LoggingAContactPutsItInTheLogAndClearsTheEntry() { RadioPosition session = Session(); Type(session, "JA1XYZ", "25"); Qso logged = session.LogContact(); Assert.Equal("JA1XYZ", logged.Call.Text); Assert.Equal(25, logged.Zone); Assert.Equal(3, logged.Points); Assert.Equal("", session.Entry.Call); Assert.Single(session.Log.Qsos); } [Fact] public void CountryAndPrefixAreFilledInFromTheCountryFile() { RadioPosition session = Session(); Type(session, "JA1XYZ", "25"); Qso logged = session.LogContact(); Assert.Equal("JA", logged.CountryPrefix); Assert.Equal("AS", logged.Continent); Assert.Equal("JA1", logged.WpxPrefix); } [Fact] public void AnIncompleteExchangeIsNotLogged() { RadioPosition session = Session(); session.Entry.Call = "JA1XYZ"; Assert.Throws(() => session.LogContact()); } [Fact] public void TheSameStationOnTheSameBandReadsAsADupe() { RadioPosition session = Session(); Type(session, "JA1XYZ", "25"); session.LogContact(); Type(session, "JA1XYZ", "25"); Assert.True(session.Verdict()?.IsDupe); } [Fact] public void MovingBandClearsTheDupe() { RadioPosition session = Session(); Type(session, "JA1XYZ", "25"); session.LogContact(); session.Tune(Frequency.FromKilohertz(7_025)); Type(session, "JA1XYZ", "25"); Assert.False(session.Verdict()?.IsDupe); } [Fact] public void SerialNumbersCountUp() { RadioPosition session = Session(new CqWpx(ModeCategory.Cw)); Assert.Equal(1, session.SentNumber); session.Entry.Call = "JA1XYZ"; session.Entry.Set(ExchangeSlot.ReceivedReport, "599"); session.Entry.Set(ExchangeSlot.SerialNumber, "12"); session.LogContact(); Assert.Equal(2, session.SentNumber); } [Fact] public void SpaceMovesThroughTheBoxesAndBackToTheCall() { RadioPosition session = Session(); Assert.Equal(0, session.Entry.Focus); session.Entry.Advance(); Assert.Equal(1, session.Entry.Focus); session.Entry.Advance(); session.Entry.Advance(); Assert.Equal(0, session.Entry.Focus); } [Theory] [InlineData("14025", 14_025_000)] [InlineData("14.025", 14_025_000)] [InlineData("7025.5", 7_025_500)] public void AFrequencyTypedIntoTheCallBoxIsRecognised(string typed, long hertz) { RadioPosition session = Session(); session.Entry.Call = typed; Assert.Equal(hertz, session.PendingQsy()?.Hertz); } [Theory] [InlineData("JA1XYZ")] [InlineData("12000")] public void ACallOrAnOutOfBandNumberIsNotAQsy(string typed) { RadioPosition session = Session(); session.Entry.Call = typed; Assert.Null(session.PendingQsy()); } [Fact] public void AContactDeletedFromTheLogGivesItsMultiplierBack() { RadioPosition session = Session(); Type(session, "JA1XYZ", "25"); Qso first = session.LogContact(); Type(session, "JA2XYZ", "25"); session.LogContact(); Assert.Equal(2, session.Log.Tally.TotalMultipliers); session.Session.Delete(first.Id); Assert.Single(session.Log.Qsos); Assert.Equal(2, session.Log.Tally.TotalMultipliers); Assert.True(session.Log.Qsos.Single().IsMultiplier1); } [Fact] public void AContactWorkedSplitRecordsWhereWeTransmitted() { RadioPosition session = Session(); session.Tune( Frequency.FromKilohertz(14_025), Modes.Cw, Frequency.FromKilohertz(14_200)); Type(session, "JA1XYZ", "25"); Qso logged = session.LogContact(); Assert.Equal(14_025_000, logged.Frequency.Hertz); Assert.Equal(14_200_000, logged.QsxFrequency.Hertz); } [Fact] public void AContactRecordsWhichRadioMadeIt() { RadioPosition session = new(Shared(new FakeLogStore()), 2); Type(session, "JA1XYZ", "25"); Assert.Equal(2, session.LogContact().RadioNumber); } /// Tuning without saying anything about split leaves it where it was, so a /// frequency typed by hand does not clear what the radio reported. [Fact] public void TuningWithoutASplitLeavesTheOneAlreadySet() { RadioPosition session = Session(); session.Tune(Frequency.FromKilohertz(14_025), null, Frequency.FromKilohertz(14_200)); session.Tune(Frequency.FromKilohertz(14_030)); Assert.Equal(14_200_000, session.TransmitFrequency.Hertz); } [Fact] public void EditingAContactRescoresTheLog() { RadioPosition session = Session(); Type(session, "JA1XYZ", "25"); Qso logged = session.LogContact(); Assert.Equal(3, session.Log.Tally.Points); QsoEdit edit = session.Session.Edit(logged.Id, QsoField.Call, "DL2XYZ"); Assert.True(edit.IsAccepted); Assert.Equal("DL2XYZ", session.Log.Qsos.Single().Call.Text); Assert.Equal(0, session.Log.Tally.Points); } [Fact] public void ARefusedEditLeavesTheLogAlone() { RadioPosition session = Session(); Type(session, "JA1XYZ", "25"); Qso logged = session.LogContact(); QsoEdit edit = session.Session.Edit(logged.Id, QsoField.Zone, "41"); Assert.False(edit.IsAccepted); Assert.Equal(25, session.Log.Qsos.Single().Zone); } /// The other stations need the call and time the contact had before, because /// that is how they find their own copy of it. [Fact] public void AnEditReportsWhatTheContactUsedToBe() { RadioPosition session = Session(); Type(session, "JA1XYZ", "25"); Qso logged = session.LogContact(); QsoChange? change = null; session.Session.Edited += (_, c) => change = c; session.Session.Edit(logged.Id, QsoField.Call, "JA2XYZ"); Assert.Equal("JA1XYZ", change?.OldCall); Assert.Equal(logged.TimestampUtc, change?.OldTimestampUtc); Assert.Equal("JA2XYZ", change?.Qso.Call.Text); } /// The country file gets a call wrong now and then. Correcting the country /// by hand has to change the score, or the correction is decoration. [Fact] public void CorrectingTheCountryChangesTheScore() { RadioPosition session = Session(); Type(session, "JA1XYZ", "25"); Qso logged = session.LogContact(); Assert.Equal(3, session.Log.Tally.Points); session.Session.Edit(logged.Id, QsoField.CountryPrefix, "DL"); Assert.Equal(0, session.Log.Tally.Points); } [Fact] public void ADeleteReportsTheContactThatWent() { RadioPosition session = Session(); Type(session, "JA1XYZ", "25"); Qso logged = session.LogContact(); Qso? gone = null; session.Session.Deleted += (_, q) => gone = q; session.Session.Delete(logged.Id); Assert.Equal("JA1XYZ", gone?.Call.Text); } [Fact] public void StoredContactsAreReadBackWhenTheSessionStarts() { FakeLogStore store = new(); RadioPosition first = new(Shared(store)); Type(first, "JA1XYZ", "25"); first.LogContact(); RadioPosition second = new(new ContestSession( store, new CqWorldWide(ModeCategory.Cw), first.Instance, Me, CountryFile.Parse(Countries))); Assert.Single(second.Log.Qsos); Assert.Equal(3, second.Log.Tally.Points); } private static RadioPosition WithHistory(Contest contest, string file) { FakeLogStore store = new(); ContestInstance instance = store.AddContest(new ContestInstance { ContestNumber = 0, ContestName = contest.Name, }); return new RadioPosition(new ContestSession( store, contest, instance, Me, CountryFile.Parse(Countries), CallHistory.Parse(file))); } [Fact] public void TheCallHistoryFillsTheExchangeIn() { RadioPosition session = WithHistory( new Sweepstakes(ModeCategory.Cw), "!!Order!!,Call,Sect,CK\nW3LPL,MDC,56"); session.Entry.Call = "W3LPL"; Assert.True(session.FillFromHistory()); Assert.Equal("MDC", session.Entry.ValueOf(ExchangeSlot.Section)); Assert.Equal("56", session.Entry.ValueOf(ExchangeSlot.Check)); } /// What the other station actually sends beats what was published, so a box /// with something in it is left alone. [Fact] public void WhatIsAlreadyTypedIsNotOverwritten() { RadioPosition session = WithHistory( new Sweepstakes(ModeCategory.Cw), "!!Order!!,Call,Sect\nW3LPL,MDC"); session.Entry.Call = "W3LPL"; session.Entry.Set(ExchangeSlot.Section, "EPA"); session.FillFromHistory(); Assert.Equal("EPA", session.Entry.ValueOf(ExchangeSlot.Section)); } [Fact] public void ACallTheHistoryDoesNotHoldFillsNothing() { RadioPosition session = WithHistory( new Sweepstakes(ModeCategory.Cw), "!!Order!!,Call,Sect\nW3LPL,MDC"); session.Entry.Call = "JA1XYZ"; Assert.False(session.FillFromHistory()); Assert.Equal("", session.Entry.ValueOf(ExchangeSlot.Section)); } /// CQ WW exchanges the CQ zone, so the CQ zone column is the one to use. [Fact] public void TheZoneComesFromTheColumnTheContestAsksFor() { RadioPosition session = WithHistory( new CqWorldWide(ModeCategory.Cw), "!!Order!!,Call,CqZone,ITUZone\nJA1XYZ,25,45"); session.Entry.Call = "JA1XYZ"; session.FillFromHistory(); Assert.Equal("25", session.Entry.ValueOf(ExchangeSlot.Zone)); } /// A file for a section contest often carries the state instead. [Fact] public void TheStateFillsASectionWhenTheFileHasNoSection() { RadioPosition session = WithHistory( new Sweepstakes(ModeCategory.Cw), "!!Order!!,Call,Sect,State\nK1TTT,,CT"); session.Entry.Call = "K1TTT"; session.FillFromHistory(); Assert.Equal("CT", session.Entry.ValueOf(ExchangeSlot.Section)); } [Fact] public void TheNameAndCommentBoxesReachTheLog() { RadioPosition session = Session(); session.Entry.Call = "JA1XYZ"; session.Entry.Set(ExchangeSlot.ReceivedReport, "599"); session.Entry.Set(ExchangeSlot.Zone, "25"); session.OtherName = "Ken"; session.Comment = "up 2"; Qso logged = session.LogContact(); Assert.Equal("Ken", logged.Name); Assert.Equal("up 2", logged.Comment); Assert.Equal("", session.OtherName); Assert.Equal("", session.Comment); } [Fact] public void WipingClearsTheNameAndTheComment() { RadioPosition session = Session(); session.OtherName = "Ken"; session.Comment = "up 2"; session.Wipe(); Assert.Equal("", session.OtherName); Assert.Equal("", session.Comment); } }