package main

import (
	"errors"
	"sync"
	"testing"
	"time"
)

// The coupling is a SET, not a global switch.
//
// A station can run a combiner pair AND a third amplifier that has nothing to
// do with it — two SPE on the combiner, a PowerGenius on another antenna. A
// global flag would send that third one into OPERATE alongside them.
func TestAmpTargetsFollowTheGroup(t *testing.T) {
	a := &App{}
	a.ampInsts = map[string]*ampInst{"spe1": {}, "spe2": {}, "pgxl": {}}
	group := []string{"spe1", "spe2"}

	// A member commands the whole group, itself first.
	got := a.ampTargets("spe2", group)
	if len(got) != 2 || got[0] != "spe2" || got[1] != "spe1" {
		t.Errorf("member = %v, want [spe2 spe1] — the clicked one first", got)
	}

	// The amplifier OUTSIDE the group keeps its buttons to itself.
	if got := a.ampTargets("pgxl", group); len(got) != 1 || got[0] != "pgxl" {
		t.Errorf("outsider = %v, want just [pgxl]", got)
	}

	// No group at all: everyone is on their own.
	if got := a.ampTargets("spe1", nil); len(got) != 1 || got[0] != "spe1" {
		t.Errorf("no group = %v, want just [spe1]", got)
	}

	// A group remembering an amplifier that is gone must not carry it: it would
	// fail the command for a member that no longer exists.
	if got := a.ampTargets("spe1", []string{"spe1", "deleted"}); len(got) != 1 || got[0] != "spe1" {
		t.Errorf("stale member = %v, want it dropped", got)
	}
}

// One amplifier coupled to itself is not a group — storing it would make every
// command fan out to a single member for ever, which is just noise.
func TestLinkedAmpsNeedsTwo(t *testing.T) {
	for _, tc := range []struct {
		in   []string
		want int
	}{
		{[]string{"a", "b"}, 2},
		{[]string{"a"}, 0},
		{[]string{"  ", "a"}, 0}, // blanks are not members
		{nil, 0},
	} {
		clean := make([]string, 0, len(tc.in))
		for _, id := range tc.in {
			if id != "" && id != "  " {
				clean = append(clean, id)
			}
		}
		if len(clean) < 2 {
			clean = nil
		}
		if len(clean) != tc.want {
			t.Errorf("SetLinkedAmps(%v) would keep %d, want %d", tc.in, len(clean), tc.want)
		}
	}
}

// Two combined amplifiers must be commanded AT THE SAME TIME, not one after the
// other.
//
// Sequentially, the second was commanded only once the first had answered — and
// an SPE answers over its own link, in its own time. The combiner heard power
// appear on one input before the other and beeped about it, on every OFF and
// every ON. This is what an operator hears, so it is worth a test that would
// hear it too.
func TestLinkedAmpCommandsLeaveTogether(t *testing.T) {
	a := &App{}
	const slow = 150 * time.Millisecond

	var mu sync.Mutex
	starts := map[string]time.Time{}
	err := a.ampFanOut([]string{"one", "two"}, func(id string) error {
		mu.Lock()
		starts[id] = time.Now()
		mu.Unlock()
		time.Sleep(slow) // an amplifier taking its time to answer
		return nil
	})
	if err != nil {
		t.Fatalf("fan-out: %v", err)
	}
	if len(starts) != 2 {
		t.Fatalf("%d amplifiers were commanded, want both", len(starts))
	}
	// Both goroutines wait on one channel and are released by closing it, so the
	// gap is scheduling noise. Sequential execution would put a full command
	// between them.
	gap := starts["one"].Sub(starts["two"])
	if gap < 0 {
		gap = -gap
	}
	if gap > slow/3 {
		t.Errorf("the two amplifiers were commanded %v apart — the combiner hears that as one input arriving late", gap)
	}
}

// The amplifier the operator clicked comes first, and its failure is the one
// reported: "the amp I pressed did not respond" beats the same message about
// its silent partner.
func TestLinkedAmpErrorNamesTheOneClicked(t *testing.T) {
	a := &App{}
	clicked := errors.New("the one clicked")
	other := errors.New("the other one")
	err := a.ampFanOut([]string{"clicked", "other"}, func(id string) error {
		if id == "clicked" {
			return clicked
		}
		return other
	})
	if !errors.Is(err, clicked) {
		t.Errorf("fan-out reported %v, want the amplifier the operator pressed", err)
	}
}

// One amplifier is the ordinary case and must not change: run inline, no
// goroutine, no barrier, and the error straight back.
func TestSingleAmpRunsInline(t *testing.T) {
	a := &App{}
	boom := errors.New("not running")
	if err := a.ampFanOut([]string{"solo"}, func(string) error { return boom }); !errors.Is(err, boom) {
		t.Errorf("a single amplifier reported %v, want the error itself", err)
	}
	if err := a.ampFanOut(nil, func(string) error { return boom }); err != nil {
		t.Errorf("an empty group reported %v, want nothing to do", err)
	}
}
