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)
	}
}
