package main import ( "testing" "hamlog/internal/qso" ) // idx builds a worked-grid index the way GridWorkedIndex does, so the test // exercises the real key shape rather than a convenient one. func idx(entries ...struct { grid, band, mode string confirmed bool }) map[string]bool { m := map[string]bool{} for _, e := range entries { for _, cls := range gridClassesOf(e.mode) { for _, k := range []string{qso.GridKey(e.grid, e.band, cls), qso.GridKey(e.grid, "", cls)} { m[k] = m[k] || e.confirmed } } } return m } type entry = struct { grid, band, mode string confirmed bool } // The six scopes are six different questions, and the whole point of offering // them is that the same square answers differently. func TestGridScopes(t *testing.T) { // JN36 worked once: 20 m, FT8, not confirmed. i := idx(entry{"JN36", "20m", "FT8", false}) for _, tc := range []struct { scope string band string mode string want bool why string }{ {"band_ftx", "20m", "FT8", false, "same band, same family — already mine"}, {"band_ftx", "15m", "FT8", true, "another band, and the scope counts bands"}, {"mix_ftx", "15m", "FT8", false, "another band, but this scope does not care"}, {"band_mode", "20m", "FT4", true, "same band, but FT4 is not FT8 under an exact-mode scope"}, {"band_ftx", "20m", "FT4", false, "same band, and FT4 is in the same FT family"}, {"band_digi", "20m", "RTTY", false, "digital counts every digital mode"}, {"band_ftx", "20m", "RTTY", false, "RTTY is outside an FTx scope — it is not evidence either way, so nothing is flagged"}, {"mix_digi", "15m", "FT4", false, "the widest scope: worked anywhere, any digital"}, } { got := gridIsWanted(i, lookupGridScope(tc.scope), "new", "JN36", tc.band, tc.mode) if got != tc.want { t.Errorf("scope %s, %s %s → wanted=%v, want %v — %s", tc.scope, tc.band, tc.mode, got, tc.want, tc.why) } } } // "New + unconfirmed" chases a square that is worked but not yet confirmed: it // is still missing from the award, which is the only sense in which it is done. func TestGridHuntUnconfirmed(t *testing.T) { unconf := idx(entry{"JN36", "20m", "FT8", false}) conf := idx(entry{"IO91", "20m", "FT8", true}) if gridIsWanted(unconf, lookupGridScope("band_ftx"), "new", "JN36", "20m", "FT8") { t.Error("worked-but-unconfirmed must NOT be wanted when hunting only what is new") } if !gridIsWanted(unconf, lookupGridScope("band_ftx"), "new_unconfirmed", "JN36", "20m", "FT8") { t.Error("worked-but-unconfirmed must BE wanted when hunting unconfirmed too") } if gridIsWanted(conf, lookupGridScope("band_ftx"), "new_unconfirmed", "IO91", "20m", "FT8") { t.Error("a confirmed square is finished under either hunt") } } // A mode the scope cannot judge flags nothing rather than guessing: a phone // contact under an FTx scope is not evidence either way. func TestGridScopeIgnoresModesItCannotJudge(t *testing.T) { i := idx(entry{"JN36", "20m", "FT8", false}) for _, mode := range []string{"SSB", "CW", ""} { if gridIsWanted(i, lookupGridScope("band_ftx"), "new", "KO01", "20m", mode) { t.Errorf("mode %q is outside an FTx scope — nothing should be flagged", mode) } } } // An unknown or unset scope falls back rather than answering "wanted" for // everything, which is what an empty class would have done. func TestGridScopeFallsBack(t *testing.T) { if got := lookupGridScope("").Key; got != defaultGridScope { t.Errorf("unset scope = %q, want the default %q", got, defaultGridScope) } if got := lookupGridScope("nonsense").Key; got != defaultGridScope { t.Errorf("unknown scope = %q, want the default %q", got, defaultGridScope) } if got := gridHunt("nonsense"); got != "new" { t.Errorf("unknown hunt = %q, want new", got) } } // The two wanted states must be told apart, not merged. A square worked an hour // ago and awaiting a QSL is a different job from one never worked, and reporting // both as plain NEW GRID is what made a station the log plainly shows as worked // still wear the badge. func TestGridStateSeparatesNewFromUnconfirmed(t *testing.T) { i := idx( entry{"JN74", "20m", "FT8", false}, // worked, no confirmation yet entry{"IO91", "20m", "FT8", true}, // worked and confirmed ) s := lookupGridScope("band_ftx") if got := gridStateFor(i, s, "new_unconfirmed", "KO99", "20m", "FT8"); got != gridStateNew { t.Errorf("never worked → %q, want %q", got, gridStateNew) } if got := gridStateFor(i, s, "new_unconfirmed", "JN74", "20m", "FT8"); got != gridStateUnconf { t.Errorf("worked but unconfirmed → %q, want %q", got, gridStateUnconf) } if got := gridStateFor(i, s, "new_unconfirmed", "IO91", "20m", "FT8"); got != "" { t.Errorf("confirmed → %q, want nothing to chase", got) } // Under the plain "new" hunt an unconfirmed square is finished business. if got := gridStateFor(i, s, "new", "JN74", "20m", "FT8"); got != "" { t.Errorf("unconfirmed under the new-only hunt → %q, want nothing", got) } }