package main

import (
	"strings"
	"testing"
)

// A colour the Flex refuses is a spot that never appears, and the command error
// goes to a log nobody is reading. Anything that is not #AARRGGBB must be
// dropped here rather than sent.
func TestHexARGB(t *testing.T) {
	for _, ok := range []string{"#FFFF3B30", "#00000000", "#ffabcdef", "#40FF6B22"} {
		if !hexARGB(ok) {
			t.Errorf("%q should be accepted", ok)
		}
	}
	for _, bad := range []string{
		"", "#FFF", "#FF3B30", "FFFF3B30", "#FFFF3B3", "#FFFF3B300",
		"#GGFF3B30", "#FFFF3B3G", "red", "rgb(255,0,0)",
	} {
		if hexARGB(bad) {
			t.Errorf("%q should be refused", bad)
		}
	}
}

// The stored palette must survive a round trip, and must not carry a status the
// resolver does not know — an unknown key would be a colour nothing can ever use
// and a field nobody can remove from the panel.
func TestNormSpotColors(t *testing.T) {
	in := SpotColors{Enabled: true, Colors: map[string]SpotColor{
		"new":      {Text: "#FFFF3B30", Bg: "#40FF3B30"},
		"new-band": {Text: "not a colour", Bg: "#40FFCC00"}, // half valid
		"worked":   {Text: "", Bg: ""},                      // nothing at all
		"invented": {Text: "#FF000000"},                     // not a status
	}}
	out := normSpotColors(in)
	if got := out.Colors["new"].Text; got != "#FFFF3B30" {
		t.Errorf("valid pair lost: %q", got)
	}
	if c := out.Colors["new-band"]; c.Text != "" || c.Bg != "#40FFCC00" {
		t.Errorf("half-valid pair = %+v, want the background kept and the text dropped", c)
	}
	if _, ok := out.Colors["worked"]; ok {
		t.Errorf("an entry with no colour at all must not be stored")
	}
	if _, ok := out.Colors["invented"]; ok {
		t.Errorf("an unknown status must not be stored")
	}
}

func TestSpotComment(t *testing.T) {
	// What the panadapter shows: the cluster's words, who spotted it, the entity,
	// then what it is worth — DXHunter's order, so an operator reads one
	// convention and not two.
	got := spotComment("CQ up 2", "F4BPO", "Franz Josef Land", "new-slot")
	want := "CQ up 2 [F4BPO] [Franz Josef Land] [New Slot]"
	if plain(got) != want {
		t.Fatalf("got %q, want %q", plain(got), want)
	}
	// EVERY space is non-breaking, the ones inside the cluster comment included:
	// SmartSDR splits its command line on spaces, so a real one truncates the
	// comment at the first word.
	if strings.Contains(got, " ") {
		t.Fatalf("an ordinary space survived: %q", got)
	}
	if !strings.Contains(got, " ") {
		t.Fatal("no non-breaking space in the comment")
	}
	// Missing parts leave no empty brackets and no double separators.
	if p := plain(spotComment("", "", "", "new")); p != "[New DXCC]" {
		t.Fatalf("got %q", p)
	}
	if p := plain(spotComment("CQ", "", "Spain", "")); p != "CQ [Spain]" {
		t.Fatalf("got %q", p)
	}
	// A status with nothing to announce contributes no tag.
	if p := plain(spotComment("loud", "DL1ABC", "Germany", "none")); p != "loud [DL1ABC] [Germany]" {
		t.Fatalf("got %q", p)
	}
	// Every colourable status except the unresolved one is labelled, or the
	// palette would say something the text does not.
	for _, st := range spotColorOrder {
		if st == "none" {
			continue
		}
		if spotStatusTag(st) == "" {
			t.Fatalf("status %q has a colour but no tag", st)
		}
	}
}

// plain puts the ordinary spaces back, for readable assertions.
func plain(s string) string { return strings.ReplaceAll(s, " ", " ") }

// The per-status switch must survive a save with no colours on that row: it is
// an instruction in its own right, and dropping it would quietly turn the status
// back on. And a palette written before the switch existed must keep sending
// everything — "no field" is not "the operator turned this off".
func TestSpotColorHideSurvivesNormalisation(t *testing.T) {
	in := SpotColors{Enabled: true, Colors: map[string]SpotColor{
		"worked":   {Hide: true},                    // no colours, just the switch
		"new":      {Text: "#FFFF3B30", Hide: true}, // both
		"none":     {Text: "not a colour"},          // neither survives
		"new-band": {Text: "#FFFFCC00"},             // colour only
	}}
	out := normSpotColors(in)
	if c, ok := out.Colors["worked"]; !ok || !c.Hide {
		t.Error("a status hidden with no colour was dropped")
	}
	if c := out.Colors["new"]; !c.Hide || c.Text != "#FFFF3B30" {
		t.Errorf("hide and colour did not both survive: %+v", c)
	}
	if _, ok := out.Colors["none"]; ok {
		t.Error("an entry with nothing valid in it was kept")
	}
	if c := out.Colors["new-band"]; c.Hide {
		t.Error("a colour-only entry came back hidden")
	}
	// An older palette says nothing about hiding, and must send everything.
	old := normSpotColors(SpotColors{Enabled: true, Colors: map[string]SpotColor{"new": {Text: "#FFFF3B30"}}})
	if old.Colors["new"].Hide {
		t.Error("a palette from before the switch existed came back hidden")
	}
}

// SmartSDR cuts a long comment mid-word — one came back from the radio as
// "…[European Russia", losing its closing bracket and its status. What the
// operator would keep must therefore survive: the spotter, the entity and the
// status, with the cluster's own words trimmed to fit.
func TestSpotCommentFitsTheRadio(t *testing.T) {
	long := "FT8    5 dB  LN14    CQ DX FROM A VERY TALKATIVE SKIMMER NODE INDEED"
	got := plain(spotComment(long, "OH6BG-#", "European Russia", "new-band"))
	if len(got) > panCommentMax {
		t.Fatalf("comment is %d chars, over the %d the radio keeps: %q", len(got), panCommentMax, got)
	}
	for _, must := range []string{"[OH6BG-#]", "[European Russia]", "[New Band]"} {
		if !strings.Contains(got, must) {
			t.Errorf("%s was trimmed away — the cluster text should go first: %q", must, got)
		}
	}
	// A short comment is left exactly as it is.
	short := plain(spotComment("CQ", "K1ABC", "Spain", "new"))
	if short != "CQ [K1ABC] [Spain] [New DXCC]" {
		t.Fatalf("got %q", short)
	}
	// No room at all for the cluster's words: they go, the brackets stay.
	tight := plain(spotComment("CQ DX UP 2 LISTENING", "VERYLONGSPOTTER-#", "Democratic Republic of the Congo", "new"))
	if strings.Contains(tight, "LISTENING") {
		t.Errorf("kept the cluster text at the expense of the tags: %q", tight)
	}
}

// RBN lines are column-aligned, so a comment arrives as "FT8    5 dB  LN14
// CQ". Preserved as non-breaking spaces, those runs rendered on the panadapter
// as a gap wide enough to push the rest of the comment off the screen.
func TestSpotCommentCollapsesClusterPadding(t *testing.T) {
	got := plain(spotComment("FT8   -13 dB          CQ", "WC2L-#", "United States", "new"))
	want := "FT8 -13 dB CQ [WC2L-#] [United States] [New DXCC]"
	if got != want {
		t.Fatalf("got  %q\nwant %q", got, want)
	}
	// Tabs and newlines are padding too.
	if p := plain(spotComment("CQ\t\tDX\n", "K1ABC", "", "")); p != "CQ DX [K1ABC]" {
		t.Fatalf("got %q", p)
	}
}
