using Nonemm.Contests; using Nonemm.Contests.Rules; using Nonemm.Core; using Nonemm.Core.Country; using Nonemm.Storage; namespace Nonemm.Session.Tests; /// What a contact from another station of a multi-operator entry does to this /// log. public class NetworkedContactsTests { 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 readonly DateTime Start = new(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc); private static (ContestSession Session, FakeLogStore Store) Station() { FakeLogStore store = new(); ContestInstance instance = store.AddContest(new ContestInstance { ContestNumber = 0, ContestName = "CQWW", }); return ( new ContestSession( store, new CqWorldWide(ModeCategory.Cw), instance, Me, CountryFile.Parse(Countries)), store); } private static Qso Arriving(string call, int minute, int zone = 25) => new() { Id = $"{call}-{minute}", TimestampUtc = Start.AddMinutes(minute), Call = Callsign.Parse(call), Frequency = Frequency.FromKilohertz(14_030), Mode = Modes.Cw, ContestName = "CQWW", SentReport = "599", ReceivedReport = "599", Zone = zone, // what the sending station worked out, which is not trusted Points = 99, IsMultiplier1 = true, }; /// A contact worked at this station, which `Add` scores from the rules like /// any other. private static Qso WorkHere(ContestSession session, string call, int minute, int zone = 25) => session.Add(Arriving(call, minute, zone) with { Id = Qso.NewId(), ContestNumber = session.Instance.ContestNumber, }); [Fact] public void AContactFromAnotherStationIsLoggedAndScoredHere() { (ContestSession session, _) = Station(); Assert.True(session.AddFromNetwork(Arriving("JA1XYZ", 1))); Qso stored = Assert.Single(session.Log.Qsos); Assert.Equal("JA1XYZ", stored.Call.Text); Assert.Equal(3, stored.Points); Assert.False(stored.IsOriginal); Assert.Equal(session.Instance.ContestNumber, stored.ContestNumber); } [Fact] public void ItDoesNotGoBackOutToTheOtherStations() { (ContestSession session, _) = Station(); int sent = 0; session.Logged += (_, _) => sent++; session.AddFromNetwork(Arriving("JA1XYZ", 1)); Assert.Equal(0, sent); } [Fact] public void TheSameContactArrivingTwiceIsTakenOnce() { (ContestSession session, _) = Station(); session.AddFromNetwork(Arriving("JA1XYZ", 1)); Assert.False(session.AddFromNetwork(Arriving("JA1XYZ", 1))); Assert.Single(session.Log.Qsos); } /// The station that worked it first keeps the multiplier, however late the /// message about it turns up. [Fact] public void AContactThatArrivesLateStillTakesTheMultiplierItWorkedFirst() { (ContestSession session, _) = Station(); WorkHere(session, "JA9ZZZ", minute: 5); session.AddFromNetwork(Arriving("JA1XYZ", minute: 1)); Assert.True(session.Log.Qsos[0].IsMultiplier1); Assert.False(session.Log.Qsos[1].IsMultiplier1); Assert.Equal("JA1XYZ", session.Log.Qsos[0].Call.Text); } [Fact] public void WhatIsBeingTypedIsLeftAlone() { (ContestSession session, _) = Station(); OperatingPosition position = new(session); position.Entry.Call = "G4AB"; position.Entry.Set(ExchangeSlot.Zone, "14"); session.AddFromNetwork(Arriving("JA1XYZ", 1)); Assert.Equal("G4AB", position.Entry.Call); Assert.Equal("14", position.Entry.ValueOf(ExchangeSlot.Zone)); } /// The stored points and multiplier flags go stale when a contact from /// another station arrives after the one that took the multiplier. The /// rescore is what writes them back, because N1MM reads those columns /// rather than working them out. [Fact] public void ARescoreWritesTheScoringBackToTheStore() { (ContestSession session, FakeLogStore store) = Station(); Qso mine = WorkHere(session, "JA9ZZZ", minute: 5); session.AddFromNetwork(Arriving("JA1XYZ", minute: 1)); Assert.True(store.Qsos(session.Instance.ContestNumber).Single(q => q.Id == mine.Id).IsMultiplier1); Assert.Equal(2, session.Rescore(DateTime.MinValue)); Qso stored = store.Qsos(session.Instance.ContestNumber).Single(q => q.Id == mine.Id); Assert.False(stored.IsMultiplier1); Assert.Equal(session.Log.Qsos.Single(q => q.Id == mine.Id).Points, stored.Points); } /// A rescore of the last hour leaves the rows before it alone. [Fact] public void ARescoreOnlyWritesTheContactsInsideThePeriod() { (ContestSession session, _) = Station(); WorkHere(session, "JA9ZZZ", minute: 5); WorkHere(session, "JA8YYY", minute: 90); Assert.Equal(1, session.Rescore(Start.AddMinutes(60))); } /// A resync sends its contacts in a burst, and they are scored together. [Fact] public void ABurstOfContactsIsLoggedOnceAndRaisesOneChange() { (ContestSession session, FakeLogStore store) = Station(); int changes = 0; session.Changed += (_, _) => changes++; int added = session.AddRangeFromNetwork( [Arriving("JA1XYZ", 1), Arriving("JA2XYZ", 2), Arriving("JA1XYZ", 1)]); Assert.Equal(2, added); Assert.Equal(1, changes); Assert.Equal(2, store.Qsos(session.Instance.ContestNumber).Count); Assert.Equal(2, session.Log.Qsos.Count); } /// A resync drops the block it is about to be sent again, so the contacts /// are replaced rather than added to. [Fact] public void ABlockOfOneStationsContactsCanBeCleared() { (ContestSession session, FakeLogStore store) = Station(); session.AddFromNetwork(Arriving("JA1XYZ", minute: 1) with { StationName = "RUN-PC" }); session.AddFromNetwork(Arriving("JA2XYZ", minute: 90) with { StationName = "RUN-PC" }); session.AddFromNetwork(Arriving("JA3XYZ", minute: 2) with { StationName = "MULT-PC" }); Assert.Equal(1, session.ClearFromNetwork("run-pc", Start, TimeSpan.FromHours(1))); Assert.Equal(["JA2XYZ", "JA3XYZ"], session.Log.Qsos.Select(q => q.Call.Text).Order()); Assert.Equal(2, store.Qsos(session.Instance.ContestNumber).Count); } /// Reset Serial Numbers at all Stations puts the next number back to the /// highest in the log plus one, which is what the N1MM manual says it does: /// every station lands on the same number without repeating one that has /// gone out. The contacts already logged keep the numbers they were sent /// with. [Fact] public void ResettingTheSerialNumbersCountsFromTheLogAgain() { (ContestSession session, _) = Station(); session.Add(Arriving("JA1XYZ", 1) with { Id = Qso.NewId(), SentNumber = 1 }); session.Add(Arriving("JA2XYZ", 2) with { Id = Qso.NewId(), SentNumber = 2 }); // a number claimed by a station that was logging something else, which // is the mess the reset is for session.Numbers.Note(400, Bands.Band20M); Assert.Equal(401, session.NextNumberOn(null)); session.RestartNumbers(); Assert.Equal(3, session.NextNumberOn(null)); Assert.Equal([1, 2], session.Log.Qsos.Select(q => q.SentNumber)); } /// Every contact logged here carries the computer that made it, which is /// what a resync asks for and what N1MM stamps into `NetBiosName`. [Fact] public void AContactLoggedHereCarriesThisComputersName() { (ContestSession session, _) = Station(); session.StationName = "RUN-PC"; session.StationNumber = 3; Qso stored = WorkHere(session, "JA1XYZ", minute: 1); Assert.Equal("RUN-PC", stored.StationName); Assert.Equal(3, stored.NetworkedComputerNumber); } [Fact] public void AnEditFromAnotherStationIsAppliedById() { (ContestSession session, _) = Station(); session.AddFromNetwork(Arriving("JA1XYZ", 1)); Assert.True(session.ReplaceFromNetwork( Arriving("JA1XYZ", 1) with { Zone = 24 }, "JA1XYZ", Start.AddMinutes(1))); Assert.Equal(24, Assert.Single(session.Log.Qsos).Zone); } /// N1MM does not know our ids, so a message from N1MM is matched on the /// call and time the contact had before the edit. [Fact] public void AnEditWithNoIdIsMatchedOnTheOldCallAndTime() { (ContestSession session, _) = Station(); Qso worked = WorkHere(session, "JA1XYZ", minute: 1); Assert.True(session.ReplaceFromNetwork( Arriving("JA1XYY", 1) with { Id = "" }, "JA1XYZ", worked.TimestampUtc)); Qso changed = Assert.Single(session.Log.Qsos); Assert.Equal("JA1XYY", changed.Call.Text); Assert.Equal(worked.Id, changed.Id); } [Fact] public void AnEditToAContactWeDoNotHoldIsRefused() { (ContestSession session, _) = Station(); Assert.False(session.ReplaceFromNetwork(Arriving("JA1XYZ", 1), "JA1XYZ", Start)); Assert.Empty(session.Log.Qsos); } [Fact] public void ADeleteFromAnotherStationRemovesTheContactAndRescores() { (ContestSession session, FakeLogStore store) = Station(); session.AddFromNetwork(Arriving("JA1XYZ", 1)); WorkHere(session, "JA9ZZZ", minute: 5); Assert.True(session.DeleteFromNetwork("JA1XYZ-1", "JA1XYZ", Start.AddMinutes(1))); Qso left = Assert.Single(session.Log.Qsos); Assert.Equal("JA9ZZZ", left.Call.Text); Assert.True(left.IsMultiplier1); Assert.Single(store.Qsos(session.Instance.ContestNumber)); } [Fact] public void ADeleteOfAContactWeDoNotHoldIsRefused() { (ContestSession session, _) = Station(); Assert.False(session.DeleteFromNetwork("nothing", "JA1XYZ", Start)); } }