package quiesce

import (
	"context"
	"log"
	"strings"
	"testing"
)

// R-88 Part 2 (controller half) — a nil age is no longer self-licensing.
//
// The valve fires only on a POSITIVE claim of "never backed up". These tests assert BEHAVIOUR (were
// stacks stopped?), never a log line — a controller that logs the right thing and then does the
// wrong thing must fail here.

// stateBackend is a tierBackend whose DueFor also reports an age_state wire string.
type stateBackend struct {
	*tierBackend
	wire map[string]string // target → age_state as sent by the agent ("" = legacy)
}

func (b *stateBackend) DueFor(ctx context.Context, target string) (bool, *int64, string, error) {
	due, age, _, err := b.tierBackend.DueFor(ctx, target)
	return due, age, b.wire[target], err
}

func stateLoop(t *testing.T, wire map[string]string, st *fakeStacks, logTo *strings.Builder) *Loop {
	t.Helper()
	be := newTierBackend()
	be.tiers = []BackupTier{{Target: "local"}}
	be.dueSet["local"] = true
	be.phases["local"] = []string{phaseDone}
	sb := &stateBackend{tierBackend: be, wire: wire}
	l := windowLoop(t, sb, st, "02:30", atBudapest(12, 0)) // 12:00 — firmly OUTSIDE [04:30, 08:30)
	if logTo != nil {
		l.logger = log.New(logTo, "", 0)
	}
	return l
}

// ── SCENARIO A — UNKNOWN does not bypass the window ──────────────────────────────────────────
//
// COMPANION RED-PROOF (observed): make valveLicensed return true for AgeStateUnknown (the pre-fix
// behaviour, where any nil age fired the valve) and this fails with
//
//	"R-88 Part 2: an UNKNOWN age bypassed the backup window and stopped 1 stack(s) — an unreadable
//	 storage must not masquerade as a first-ever backup"
//
// Restored.
func TestAgeState_UnknownDoesNotBypassTheWindow(t *testing.T) {
	st := &fakeStacks{running: []string{"bookstack"}}
	l := stateLoop(t, map[string]string{"local": "unknown"}, st, nil)

	if err := l.runOnce(context.Background()); err != nil {
		t.Fatal(err)
	}
	if got := len(st.stoppedNames()); got != 0 {
		t.Fatalf("R-88 Part 2: an UNKNOWN age bypassed the backup window and stopped %d stack(s) — "+
			"an unreadable storage must not masquerade as a first-ever backup", got)
	}
}

// ── SCENARIO B — ABSENT still runs outside the window ────────────────────────────────────────
//
// B is what makes A safe. An implementation that never licensed the valve would pass A and silently
// starve every new box.
//
// COMPANION RED-PROOF (observed): drop AgeStateAbsent from valveLicensed (keeping only legacy) and
// this fails with
//
//	"a genuine first-ever backup (absent) must RUN outside the window; 0 stack(s) stopped — the
//	 safety valve was lost and a new box would starve"
//
// Restored.
func TestAgeState_AbsentStillRunsOutsideTheWindow(t *testing.T) {
	st := &fakeStacks{running: []string{"bookstack"}}
	l := stateLoop(t, map[string]string{"local": "absent"}, st, nil)

	if err := l.runOnce(context.Background()); err != nil {
		t.Fatal(err)
	}
	if len(st.stoppedNames()) == 0 {
		t.Fatal("a genuine first-ever backup (absent) must RUN outside the window; 0 stack(s) stopped — " +
			"the safety valve was lost and a new box would starve")
	}
}

// ── SCENARIO C — old agent, new controller: TODAY'S behaviour exactly ────────────────────────
//
// Asserts BEHAVIOUR, not the degrade log line: a controller that logs the degrade and then defers
// would pass a log-only assertion while silently changing behaviour on every un-upgraded box.
//
// COMPANION RED-PROOF (observed): drop AgeStateLegacy from valveLicensed (treating a missing field
// as unknown — the "safer-looking" choice) and this fails with
//
//	"C: a pre-v0.105.0 agent must behave EXACTLY as before — nil age fires the valve. 0 stack(s)
//	 stopped; an un-upgraded box just silently stopped backing up outside its window"
//
// Restored.
func TestAgeState_LegacyAgentKeepsTodaysBehaviour(t *testing.T) {
	st := &fakeStacks{running: []string{"bookstack"}}
	var logbuf strings.Builder
	l := stateLoop(t, map[string]string{"local": ""}, st, &logbuf) // NO field on the wire

	if err := l.runOnce(context.Background()); err != nil {
		t.Fatal(err)
	}
	if len(st.stoppedNames()) == 0 {
		t.Fatal("C: a pre-v0.105.0 agent must behave EXACTLY as before — nil age fires the valve. " +
			"0 stack(s) stopped; an un-upgraded box just silently stopped backing up outside its window")
	}
	// ...and the degrade must be VISIBLE, or a fleet drifts without anyone knowing.
	if !strings.Contains(logbuf.String(), "age_state") {
		t.Fatalf("the legacy degrade must be logged once; log:\n%s", logbuf.String())
	}
}

// The degrade is logged ONCE, not every poll.
func TestAgeState_LegacyDegradeLoggedOnce(t *testing.T) {
	st := &fakeStacks{running: []string{"bookstack"}}
	var logbuf strings.Builder
	l := stateLoop(t, map[string]string{"local": ""}, st, &logbuf)
	for i := 0; i < 3; i++ {
		if err := l.runOnce(context.Background()); err != nil {
			t.Fatal(err)
		}
	}
	if n := strings.Count(logbuf.String(), "pre-v0.105.0"); n != 1 {
		t.Fatalf("the legacy degrade must be logged ONCE per process, got %d", n)
	}
}

// An unrecognised FUTURE state maps to legacy, not to unknown — a newer agent inventing a fourth
// value must not accidentally acquire "unknown" semantics from a controller that never heard of it.
func TestAgeState_UnrecognisedWireValueIsLegacy(t *testing.T) {
	for _, wire := range []string{"", "known", "absent", "unknown", "quantum", "TRUE", "0"} {
		got := ageStateFromWire(wire)
		switch wire {
		case "known", "absent", "unknown":
			if string(got) != wire {
				t.Errorf("%q must map to itself, got %q", wire, got)
			}
		default:
			if got != AgeStateLegacy {
				t.Errorf("%q must map to LEGACY (fail toward known behaviour), got %q", wire, got)
			}
		}
	}
}

// valveLicensed as a truth table — the contract, independent of the loop.
func TestAgeState_ValveLicenceTable(t *testing.T) {
	age := int64(3600)
	cases := []struct {
		name string
		t    dueTier
		want bool
	}{
		{"absent licenses", dueTier{state: AgeStateAbsent}, true},
		{"legacy licenses (un-upgraded agent keeps old behaviour)", dueTier{state: AgeStateLegacy}, true},
		{"unknown does NOT license", dueTier{state: AgeStateUnknown}, false},
		{"known with a real age needs no licence", dueTier{state: AgeStateKnown, ageSecs: &age}, false},
	}
	for _, c := range cases {
		if got := valveLicensed([]dueTier{c.t}); got != c.want {
			t.Errorf("%s: valveLicensed = %v, want %v", c.name, got, c.want)
		}
	}
	// One unknown tier must not be licensed by a sibling that is merely known-with-age.
	if valveLicensed([]dueTier{{state: AgeStateUnknown}, {state: AgeStateKnown, ageSecs: &age}}) {
		t.Error("a known sibling must not license an unknown tier's valve")
	}
}
