package backup

import (
	"context"
	"fmt"
	"sync"
	"testing"
	"time"

	"gitea.dooplex.hu/admin/felhom-agent/internal/reconcile"
)

// R-85 (1.1) — the spec is built PER RUN, never frozen at construction.
//
// It used to be an immediately-invoked function at daemon start, so storageTier() and
// restoreTaskTimeout() were evaluated ONCE and the value reused for every run for the process
// lifetime. That is what made an offsite restore-test impossible to schedule at all, and it was a
// latent staleness bug besides: a storage-type or config change did not take effect until restart.

type specSpy struct {
	mu       sync.Mutex
	calls    int
	archives []string
	tiers    []string // what the builder decided, per call
}

func (sp *specSpy) build(_ context.Context, archive string) reconcile.RestoreTestSpec {
	sp.mu.Lock()
	defer sp.mu.Unlock()
	sp.calls++
	sp.archives = append(sp.archives, archive)
	// Decide the tier from the ARCHIVE, exactly as main.go does (the v0.100.0 rule).
	tier := "local"
	if len(archive) > 10 && archive[:10] == "felhom-pbs" {
		tier = "pbs"
	}
	sp.tiers = append(sp.tiers, tier)
	return reconcile.RestoreTestSpec{
		RestoreStorage: "local-lvm", ScratchMin: 990000, ScratchMax: 990009, SourceTier: tier,
	}
}

// COMPANION RED-PROOF (observed): change Scheduler.spec back to a frozen
// `reconcile.RestoreTestSpec` value captured at construction → this fails with
// "the spec builder must run ONCE PER RUN, got 1 call(s) across 3 ticks", because a frozen value is
// evaluated exactly once no matter how many ticks fire. Restored.
func TestScheduler_SpecIsBuiltPerRun(t *testing.T) {
	sp := &specSpy{}
	rt := &fakeRTRunner{res: reconcile.RestoreTestResult{Pass: true, Verified: "boot+running"}}
	n := 0
	s := NewScheduler(SchedulerOptions{
		Runner: rt,
		Pick: func(context.Context) (string, error) {
			n++
			return fmt.Sprintf("local:backup/vzdump-lxc-9201-%d.tar.zst", n), nil
		},
		Store:   NewStore(),
		Spec:    sp.build,
		Cadence: time.Hour,
		Logger:  quiet(),
	})

	for i := 0; i < 3; i++ {
		s.tick(context.Background())
	}

	sp.mu.Lock()
	defer sp.mu.Unlock()
	if sp.calls != 3 {
		t.Fatalf("the spec builder must run ONCE PER RUN, got %d call(s) across 3 ticks", sp.calls)
	}
	// And it must see the archive THIS run picked — not a stale one.
	for i, a := range sp.archives {
		want := fmt.Sprintf("local:backup/vzdump-lxc-9201-%d.tar.zst", i+1)
		if a != want {
			t.Fatalf("run %d: builder saw archive %q, want %q — the spec is not tracking the picked archive", i+1, a, want)
		}
	}
}

// The tier must follow the ARCHIVE across runs. A builder that saw only the configured target would
// return the same tier every time — which is exactly the v0.100.0 defect that killed a 14.46 GB WAN
// restore at the 10-minute local bound.
func TestScheduler_SpecTierFollowsTheArchive(t *testing.T) {
	sp := &specSpy{}
	rt := &fakeRTRunner{res: reconcile.RestoreTestResult{Pass: true, Verified: "boot+running"}}
	archives := []string{
		"local:backup/vzdump-lxc-9201-x.tar.zst",
		"felhom-pbs:backup/ct/9201/2026-07-26T15:42:42Z",
	}
	i := 0
	s := NewScheduler(SchedulerOptions{
		Runner: rt,
		Pick: func(context.Context) (string, error) {
			a := archives[i%len(archives)]
			i++
			return a, nil
		},
		Store: NewStore(), Spec: sp.build, Cadence: time.Hour, Logger: quiet(),
	})
	s.tick(context.Background())
	s.tick(context.Background())

	sp.mu.Lock()
	defer sp.mu.Unlock()
	if len(sp.tiers) != 2 || sp.tiers[0] != "local" || sp.tiers[1] != "pbs" {
		t.Fatalf("the tier must follow the archive per run; got %v", sp.tiers)
	}
}

// A nil spec builder must SKIP loudly, not panic — a wiring bug costs a restore-test, never the
// daemon goroutine.
func TestScheduler_NilSpecSkipsInsteadOfPanicking(t *testing.T) {
	rt := &fakeRTRunner{}
	s := NewScheduler(SchedulerOptions{
		Runner: rt,
		Pick:   func(context.Context) (string, error) { return "vol", nil },
		Store:  NewStore(), Cadence: time.Hour, Logger: quiet(),
	})
	s.tick(context.Background()) // must not panic
	if rt.runs != 0 {
		t.Fatalf("a nil spec must not run a restore-test; got %d run(s)", rt.runs)
	}
}
