package main

import (
	"testing"
	"time"
)

// The panel must not become one station repeated. PSK Reporter re-reports a
// calling operator every cycle, and dozens of receivers report the same
// transmission, so a station arrives many times a minute.
func TestChaseNewStoreDeduplicates(t *testing.T) {
	s := newChaseNewStore()
	t0 := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
	row := ChaseNewSpot{Call: "VK9XX", Band: "20m", Mode: "FT8", At: t0.Format(time.RFC3339)}

	if !s.put(row, t0) {
		t.Fatal("the first sighting was rejected")
	}
	if s.put(row, t0.Add(2*time.Minute)) {
		t.Error("the same station on the same band and mode was listed twice")
	}
	// A different band is a different opportunity — a new-band slot is exactly
	// what an operator is watching for.
	other := row
	other.Band = "15m"
	if !s.put(other, t0.Add(2*time.Minute)) {
		t.Error("the same station on another band was suppressed")
	}
	// Once the window has passed it is worth showing again: the station is still
	// there, and the row it had has aged out of the list.
	if !s.put(row, t0.Add(chaseSeenTTL+time.Minute)) {
		t.Error("the station never came back after the de-duplication window")
	}
}

// The list is aged with the operator's own spot lifetime, so a station heard an
// hour ago is not offered as something to chase now.
func TestChaseNewStoreAgesOut(t *testing.T) {
	s := newChaseNewStore()
	t0 := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
	s.put(ChaseNewSpot{Call: "OLD", Band: "20m", Mode: "FT8", At: t0.Format(time.RFC3339)}, t0)
	s.put(ChaseNewSpot{Call: "NEW", Band: "20m", Mode: "FT8",
		At: t0.Add(20 * time.Minute).Format(time.RFC3339)}, t0.Add(20*time.Minute))

	got := s.list(15*time.Minute, t0.Add(21*time.Minute))
	if len(got) != 1 || got[0].Call != "NEW" {
		t.Fatalf("got %+v, want only NEW", got)
	}
	// Newest first: an operator reads the top of this list and nothing else.
	s.put(ChaseNewSpot{Call: "NEWEST", Band: "20m", Mode: "FT8",
		At: t0.Add(25 * time.Minute).Format(time.RFC3339)}, t0.Add(25*time.Minute))
	got = s.list(15*time.Minute, t0.Add(26*time.Minute))
	if len(got) != 2 || got[0].Call != "NEWEST" {
		t.Fatalf("got %+v, want NEWEST first", got)
	}
}

// The list is bounded. A widget that grows without limit under a 6 m opening
// costs memory for rows nobody will ever scroll to.
func TestChaseNewStoreIsBounded(t *testing.T) {
	s := newChaseNewStore()
	t0 := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
	for i := 0; i < chaseNewMax+50; i++ {
		at := t0.Add(time.Duration(i) * time.Second)
		s.put(ChaseNewSpot{
			Call: "S" + time.Duration(i).String(), Band: "20m", Mode: "FT8",
			At: at.Format(time.RFC3339),
		}, at)
	}
	if got := len(s.list(0, t0.Add(time.Hour))); got != chaseNewMax {
		t.Errorf("kept %d rows, want the %d cap", got, chaseNewMax)
	}
}

// The panel is for stations an operator can call. WSPR is a beacon — nobody
// answers one — and on a quiet band it would be most of the list.
func TestChaseModesExcludeBeaconsAndOddities(t *testing.T) {
	for _, m := range []string{"FT8", "FT4", "FT2", "PSK31", "RTTY"} {
		if !chaseModes[m] {
			t.Errorf("%s should be listed", m)
		}
	}
	for _, m := range []string{"WSPR", "JT65", "JT9", "FST4W", "Q65", "JS8", "MSK144", "OLIVIA", ""} {
		if chaseModes[m] {
			t.Errorf("%s should not be listed", m)
		}
	}
}

// A decode on a band the operator does not have is not an opportunity, it is a
// row in the way — PSK Reporter carries microwave segments nobody in the region
// is equipped for.
func TestChaseBandAllowed(t *testing.T) {
	a := &App{}
	// Nothing loaded yet: show rather than swallow, or the panel would look
	// broken for the first seconds after it is switched on.
	if !a.chaseBandAllowed("13cm") {
		t.Error("with no band list loaded, nothing should be filtered")
	}
	a.chaseBands = map[string]bool{"20m": true, "40m": true, "6m": true}
	for _, b := range []string{"20m", "40m", "6m", " 20M "} {
		if !a.chaseBandAllowed(b) {
			t.Errorf("%q is in the operator's list and was dropped", b)
		}
	}
	for _, b := range []string{"13cm", "23cm", "2m", "630m", ""} {
		if a.chaseBandAllowed(b) {
			t.Errorf("%q is not in the operator's list and was kept", b)
		}
	}
}
