package udp

import (
	"strings"
	"testing"
)

// The escaping rule is the whole difference between a template that works on
// the bench and one that fails on the first portable callsign.
func TestRenderEscapesForURLsOnly(t *testing.T) {
	fields := map[string]string{
		"call": "F4BPO/P", "name": "Jean Pierre", "band": "20m", "az": "137",
	}
	// UDP: the receiver wants the text as typed.
	if got := Render("<CALL>{call}</CALL>", fields, false); got != "<CALL>F4BPO/P</CALL>" {
		t.Errorf("UDP render = %q", got)
	}
	// URL: a slash in a callsign is a path separator, a space breaks the request.
	if got := Render("http://sw/a?c={call}&n={name}", fields, true); got != "http://sw/a?c=F4BPO%2FP&n=Jean+Pierre" {
		t.Errorf("URL render = %q", got)
	}
}

// A placeholder the trigger does not carry becomes empty, not the literal
// "{freq}" — otherwise the braces go down the wire and a receiver files them.
func TestRenderDropsUnknownFields(t *testing.T) {
	if got := Render("<AZIMUT>{az}</AZIMUT>{freq}", map[string]string{"az": "137"}, false); got != "<AZIMUT>137</AZIMUT>" {
		t.Errorf("got %q", got)
	}
}

// Text that merely looks like a placeholder must survive: an unclosed brace is
// content, not a broken field.
func TestRenderLeavesLiteralBraces(t *testing.T) {
	for _, c := range []struct{ in, want string }{
		{"no fields here", "no fields here"},
		{"{unclosed", "{unclosed"},
		{"a } b", "a } b"},
	} {
		if got := Render(c.in, map[string]string{}, false); got != c.want {
			t.Errorf("Render(%q) = %q, want %q", c.in, got, c.want)
		}
	}
}

// Field names are matched case- and space-insensitively: an operator typing
// {Band} or { band } means the band.
func TestRenderFieldNamesAreForgiving(t *testing.T) {
	f := map[string]string{"band": "20m"}
	for _, tmpl := range []string{"{band}", "{Band}", "{ BAND }"} {
		if got := Render(tmpl, f, false); got != "20m" {
			t.Errorf("Render(%q) = %q, want 20m", tmpl, got)
		}
	}
}

// Receivers that read a line at a time need a terminator, and a trailing space
// in a text box is invisible — hence a choice rather than something to type.
func TestLineEnding(t *testing.T) {
	for in, want := range map[string]string{
		"": "", "lf": "\n", "LF": "\n", "crlf": "\r\n", " CRLF ": "\r\n", "nonsense": "",
	} {
		if got := lineEnding(in); got != want {
			t.Errorf("lineEnding(%q) = %q, want %q", in, got, want)
		}
	}
}

// These logs get sent to whoever is helping when something breaks. Storing a
// switch password as typed was a deliberate choice for LAN gear; putting it in
// a file that travels is a different one, and was never made.
func TestRedactURLHidesThePassword(t *testing.T) {
	for in, want := range map[string]string{
		"http://admin:secret@192.168.1.50/ant?b=20m": "http://admin:***@192.168.1.50/ant?b=20m",
		// No credentials, nothing to hide — and the URL must come back untouched,
		// because it is the thing being debugged.
		"http://192.168.1.50/ant?b=20m": "http://192.168.1.50/ant?b=20m",
		// A username with no password is not a secret.
		"http://admin@192.168.1.50/ant": "http://admin@192.168.1.50/ant",
		// Not a URL at all: leave it, the operator needs to see what they typed.
		"not a url {band}": "not a url {band}",
	} {
		if got := redactURL(in); got != want {
			t.Errorf("redactURL(%q) = %q, want %q", in, got, want)
		}
	}
}

// "sent 18 bytes" says nothing about whether the substitution came out right.
// The payload is shown for custom rows and only those: an ADIF record or a
// binary WSJT frame on every QSO would bury the log.
func TestSentPreviewIsForCustomRowsOnly(t *testing.T) {
	custom := Config{ServiceType: ServiceCustom, Trigger: TriggerBandChange}
	got := sentPreview(custom, []byte("<BAND>20</BAND>\r\n"))
	if got != ` on band_change: "<BAND>20</BAND>\r\n"` {
		t.Errorf("custom preview = %q", got)
	}
	// A line ending is a choice the operator made — it has to be visible, and it
	// must not break the log line in two.
	if strings.ContainsAny(got, "\r\n") {
		t.Error("the preview put a real newline in the log line")
	}
	for _, s := range []ServiceType{ServiceDBUpdated, ServiceWSJTLog, ServiceN1MMRadio} {
		if p := sentPreview(Config{ServiceType: s}, []byte("<CALL:5>VK9XX<EOR>")); p != "" {
			t.Errorf("%s should not dump its payload, got %q", s, p)
		}
	}
}
