package main

import (
	"os"
	"strings"
	"testing"
)

// The tracking mode decides how often a motorized antenna's elements run, so a
// value that fails to parse must not silently become the most aggressive
// setting. Anything unrecognised — including the empty string every config
// written before this option existed contains — has to land on the threshold
// mode, which is exactly what those configs already did.
func TestNormMotorTrackMode(t *testing.T) {
	for _, tc := range []struct{ in, want string }{
		{"always", motorTrackAlways},
		{"ALWAYS", motorTrackAlways},
		{"  band  ", motorTrackBand},
		{"step", motorTrackStep},
		{"", motorTrackStep},          // never configured — behaves as before
		{"everytime", motorTrackStep}, // near-miss, not "always"
		{"per-band", motorTrackStep},  // near-miss, not "band"
		{"25", motorTrackStep},        // a step value fed in by mistake
	} {
		if got := normMotorTrackMode(tc.in); got != tc.want {
			t.Errorf("normMotorTrackMode(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}

// Band mode compares the band the rig is on against the band the antenna was
// last commanded for. That comparison is bandForHz, and the property it has to
// have is that two frequencies far apart within one band agree while two
// frequencies close together across a band edge do not — otherwise the antenna
// either never moves or moves on every QSY.
func TestBandForHzDrivesBandTracking(t *testing.T) {
	same := [][2]int64{
		{14000000, 14350000}, // both ends of 20 m — one band, no move
		{7000000, 7200000},   // 40 m
		{50000000, 52000000}, // 6 m, a wide one
	}
	for _, p := range same {
		if a, b := bandForHz(p[0]), bandForHz(p[1]); a != b || a == "" {
			t.Errorf("%.3f MHz is %q but %.3f MHz is %q — band mode would re-tune inside one band",
				float64(p[0])/1e6, a, float64(p[1])/1e6, b)
		}
	}
	// A small QSY that crosses from 30 m into 20 m has to read as a band change.
	if a, b := bandForHz(10150000), bandForHz(14000000); a == b {
		t.Errorf("30 m and 20 m both read %q — band mode would never re-tune between them", a)
	}
}

// A "Test connection" on a serial port must not open a port OpsLog already
// holds: a COM port has one owner, and the owner is the running antenna client.
// Building a second client made the two fight — one of them logging "Serial port
// busy" for the rest of the session.
func TestTestUltrabeamReusesTheLiveSerialClient(t *testing.T) {
	src, err := os.ReadFile("app.go")
	if err != nil {
		t.Fatal(err)
	}
	body := funcBody(t, string(src), "func (a *App) TestUltrabeam(s UltrabeamSettings) error {")
	if !strings.Contains(body, "a.liveMotorFor(s)") {
		t.Error("TestUltrabeam builds a client without first checking whether the live one already owns that port")
	}
	// The check has to come BEFORE the second client is created, or it is no check.
	if i, j := strings.Index(body, "a.liveMotorFor(s)"), strings.Index(body, "newMotorClient(s)"); i < 0 || j < 0 || i > j {
		t.Error("the live-client check must precede newMotorClient")
	}
}

// funcBody returns the source of a function, up to the closing brace in column 0.
func funcBody(t *testing.T, src, signature string) string {
	t.Helper()
	i := strings.Index(src, signature)
	if i < 0 {
		t.Fatalf("function not found: %s", signature)
	}
	rest := src[i:]
	if j := strings.Index(rest, "\n}\n"); j >= 0 {
		return rest[:j]
	}
	return rest
}
