package main

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

// Every setting in OpsLog is per profile, station hardware included. A device
// started at boot and never again therefore stays on the PREVIOUS profile's
// port until Settings is opened and saved — which is how an operator running an
// SPE on COM9 for HF and another on COM10 for 6 m, one per profile, found the
// amplifier still on the old port after switching. Save is not a connect
// button.
//
// This test keeps startup and reloadAfterProfileSwitch in lockstep: anything
// started at boot must either be re-applied on a profile switch or be listed
// below with the reason it must not be. Adding a device makes the choice
// explicit instead of leaving the fifth one to be found by a user.
func TestProfileSwitchReappliesEveryStartupDevice(t *testing.T) {
	src, err := os.ReadFile("app.go")
	if err != nil {
		t.Fatalf("read app.go: %v", err)
	}

	// Started at boot but deliberately NOT re-run on a profile switch:
	notPerProfile := map[string]string{
		"startAllEnabledClusters": "the cluster panel reconnects itself; its servers are a global list",
		"startGridCache":          "a shared on-disk grid cache, not a profile's",
		"startBandOpenFeed":       "PSK Reporter, keyed on the operator grid it re-reads itself",
		// The auto-call loop is ONE goroutine for the life of the process —
		// starting a second on every profile switch would give one engine two
		// sweepers. Its settings do follow the profile: reloadAfterProfileSwitch
		// calls applyAutoCall, which re-reads them and clears the target.
		"startAutoCall": "a single sweeper goroutine; applyAutoCall in the reload carries the settings",
		// The elements and the frequency plan are FILES, shared by every
		// profile — there is one sky. What is per profile (the favourites, the
		// minimum elevation, the locator) is read live on every call, so a
		// switch is already reflected without rebuilding anything. The tracker,
		// which does transmit, IS stopped by reloadAfterProfileSwitch.
		"startSatellites": "one sky: the elements and the plan are shared files, and the per-profile settings are read live",
	}

	startup := body(t, string(src), "func (a *App) startup(ctx context.Context) {")
	reload := body(t, string(src), "func (a *App) reloadAfterProfileSwitch() {")

	call := regexp.MustCompile(`a\.(start[A-Z][A-Za-z]*)\b`)
	seen := map[string]bool{}
	for _, m := range call.FindAllStringSubmatch(startup, -1) {
		name := m[1]
		if seen[name] || notPerProfile[name] != "" {
			continue
		}
		seen[name] = true
		if !strings.Contains(reload, "a."+name) {
			t.Errorf("%s runs at startup but not on a profile switch — the device stays on the previous profile's settings.\n"+
				"Add it to reloadAfterProfileSwitch, or to notPerProfile here with the reason it must not follow the profile.", name)
		}
	}
	if len(seen) == 0 {
		t.Fatal("no startup device starters found — this test has stopped checking anything")
	}
}

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