package cat

import (
	"testing"
	"time"
)

// The rule that decides whether a silent-but-alive Icom network session is
// tolerated or torn down. It was "always tolerate", and that is what froze the
// frequency display for ever when another program took the CI-V session.
//
// The decision is pinned here rather than left implicit, because the two cases
// it separates pull in opposite directions: a rig in standby must NOT be
// reconnected every few seconds (the panel and its ON button would blink away),
// while a hijacked session must recover without restarting OpsLog.
func TestIcomSilenceTolerance(t *testing.T) {
	// tolerate mirrors the condition in ReadState.
	tolerate := func(alive, readerGone bool, lastGood time.Time, silentFor, grace time.Duration) bool {
		return alive && !readerGone && (lastGood.IsZero() || silentFor < grace)
	}
	never := time.Time{}
	some := time.Now()

	cases := []struct {
		name       string
		alive      bool
		readerGone bool
		lastGood   time.Time
		silentFor  time.Duration
		want       bool
	}{
		{"never answered since connect — rig is off, keep the panel", true, false, never, time.Hour, true},
		{"brief silence during a band change", true, false, some, 2 * time.Second, true},
		{"silence past the grace — reconnect", true, false, some, icomSilentGrace + time.Second, false},
		{"reader goroutine gone — dead however alive the link looks", true, true, some, time.Second, false},
		{"control link itself dead", false, false, some, time.Second, false},
	}
	for _, c := range cases {
		got := tolerate(c.alive, c.readerGone, c.lastGood, c.silentFor, icomSilentGrace)
		if got != c.want {
			t.Errorf("%s: tolerate = %v, want %v", c.name, got, c.want)
		}
	}
}

// The backoff must actually converge on the ceiling: a rig left switched off is
// silent for hours, and a window that stayed at 30 s would re-tear its session
// about a hundred times an hour.
func TestIcomSilenceBackoff(t *testing.T) {
	g := icomSilentGrace
	for i := 0; i < 20; i++ {
		if g < icomSilentGraceMax {
			g *= 2
		}
	}
	if g < icomSilentGraceMax {
		t.Fatalf("backoff never reached the ceiling: %s < %s", g, icomSilentGraceMax)
	}
	if g > 2*icomSilentGraceMax {
		t.Errorf("backoff overshot the ceiling: %s", g)
	}
}

// A new session must not be judged on the previous one's silence.
//
// From an operator's log: a rig switched to standby, then a dial-and-drop loop
// every forty seconds for as long as it was left there. lastGoodAt bounds "the
// link answers but no CI-V comes back"; it belongs to a session, and it was
// never cleared when a new one opened, so every fresh session started already
// past the grace — and the Icom console, where the power-ON button lives,
// blinked away on every pass.
func TestAFreshSessionStartsWithACleanSilenceClock(t *testing.T) {
	b := &IcomSerial{
		lastGoodAt:  time.Now().Add(-30 * time.Minute), // a session from before standby
		readFails:   9,
		silentGrace: icomSilentGraceMax,
	}
	// What Connect does once the transport is open, before anything is sent.
	b.lastGoodAt = time.Time{}
	b.readFails = 0
	b.silentGrace = icomSilentGrace

	if !b.lastGoodAt.IsZero() {
		t.Fatal("the previous session's last good read survived into this one")
	}
	tolerate := func(lastGood time.Time, silentFor, grace time.Duration) bool {
		return lastGood.IsZero() || silentFor < grace
	}
	if !tolerate(b.lastGoodAt, time.Hour, b.silentGrace) {
		t.Error("a session silent since connect was torn down — that is a rig in standby, and where the ON button has to work")
	}
}
