package backup

import (
	"context"
	"os"
	pathpkg "path"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
	"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)

// mandAbs builds the capture-set Abs the code produces: ComputeCaptureSet uses path.Join (slash) —
// the 3-core separator rule — so on the Windows test host the mandatory path is drive + "/rel".
func mandAbs(drive, rel string) string { return pathpkg.Join(drive, rel) }

// offbox3aProvider is a configurable StackDataProvider for the 3a capture-set tests: per-stack HDD
// path + classified binds.
type offbox3aProvider struct {
	hdd   map[string]string
	binds map[string][]ClassifiedBind
	has   map[string]bool
}

func (p *offbox3aProvider) GetStackComposePath(string) (string, bool) { return "", false }
func (p *offbox3aProvider) ListDeployedStacks() []StackSummary        { return nil }
func (p *offbox3aProvider) GetStackHDDMounts(string) []string         { return nil }
func (p *offbox3aProvider) GetStackHDDPath(n string) string           { return p.hdd[n] }
func (p *offbox3aProvider) GetDockerVolumes(string) []string          { return nil }
func (p *offbox3aProvider) StopStack(string) error                    { return nil }
func (p *offbox3aProvider) StartStack(string) error                   { return nil }
func (p *offbox3aProvider) RefreshAndIsRunning(string) bool           { return false }
func (p *offbox3aProvider) GetStackRecoveryInfo(string) (RecoveryInfo, bool) {
	return RecoveryInfo{}, false
}
func (p *offbox3aProvider) RecoverStackSecrets(string, []string) map[string]string { return nil }
func (p *offbox3aProvider) RecreateStackDefinitionFromUnit(_, _ string, _ map[string]string) error {
	return nil
}
func (p *offbox3aProvider) StartStackServices(string, []string) error { return nil }
func (p *offbox3aProvider) GetStackClassifiedBinds(n string) ([]ClassifiedBind, bool) {
	return p.binds[n], p.has[n]
}

func mandatoryHDD(rel string) ClassifiedBind {
	return ClassifiedBind{ComposeBind: appbackup.ComposeBind{Root: appbackup.RootHDD, RelPath: rel}, Class: appbackup.ClassMandatory}
}
func optionalUserdata(rel string) ClassifiedBind {
	return ClassifiedBind{ComposeBind: appbackup.ComposeBind{Root: appbackup.RootUserdata, RelPath: rel, ReadOnly: true}, Class: appbackup.ClassOptional}
}
func excludedHDD(rel string) ClassifiedBind {
	return ClassifiedBind{ComposeBind: appbackup.ComposeBind{Root: appbackup.RootHDD, RelPath: rel}, Class: appbackup.ClassExcluded}
}

// classifiedOffboxManager: a configured offbox manager + a classified provider + the drive registered
// as a schedulable storage path (so discoverOffboxUnit finds units on it).
func classifiedOffboxManager(t *testing.T, drive string) (*Manager, *settings.Settings, *offbox3aProvider) {
	t.Helper()
	m, sett := newOffboxManager(t)
	prov := &offbox3aProvider{hdd: map[string]string{}, binds: map[string][]ClassifiedBind{}, has: map[string]bool{}}
	m.SetStackProvider(prov)
	if err := sett.AddStoragePath(settings.StoragePath{Path: drive, Label: "USB", Schedulable: true}); err != nil {
		t.Fatal(err)
	}
	return m, sett, prov
}

// mkUnit lays down a discoverable recovery unit for stack on drive.
func mkUnit(t *testing.T, drive, stack string) string {
	t.Helper()
	u := RecoveryUnitPath(drive, stack)
	if err := os.MkdirAll(u, 0o755); err != nil {
		t.Fatal(err)
	}
	return u
}

// captureBackupRunner records the FULL argv of each backup call (keyed by stack tag) + forget argv, and
// answers the probes so RunOffboxBackup completes.
type backupCapture struct {
	mu      sync.Mutex
	byStack map[string][]string
	forgets [][]string
	backups int
}

func (c *backupCapture) runner() offboxRunner {
	c.byStack = map[string][]string{}
	return func(_ context.Context, _ []string, args ...string) ([]byte, error) {
		c.mu.Lock()
		defer c.mu.Unlock()
		switch {
		case contains(args, "cat") && contains(args, "config"):
			return []byte(`{"version":2}`), nil
		case contains(args, "backup"):
			c.backups++
			c.byStack[tagOf(args)] = append([]string{}, args...)
			return nil, nil
		case contains(args, "forget"):
			c.forgets = append(c.forgets, append([]string{}, args...))
			return nil, nil
		case contains(args, "snapshots"):
			return []byte(`[]`), nil
		case contains(args, "stats"):
			return []byte(`{"total_size":123}`), nil
		}
		return nil, nil
	}
}

// --- Scenario A: classified enlarged push (immich shape) ---

func TestOffbox3a_EnlargedPush_MandatoryOnly(t *testing.T) {
	drive := t.TempDir()
	m, sett, prov := classifiedOffboxManager(t, drive)
	unit := mkUnit(t, drive, "immich")
	if err := os.MkdirAll(filepath.Join(drive, "appdata", "immich"), 0o755); err != nil {
		t.Fatal(err)
	}
	// The optional :ro library exists on disk — so if the tier filter ever leaked it, the stat-filter
	// would NOT hide it (this makes the RP-A tier-filter red-proof observable).
	if err := os.MkdirAll(filepath.Join(drive, "userdata", "media", "photos"), 0o755); err != nil {
		t.Fatal(err)
	}
	prov.hdd["immich"] = drive
	prov.has["immich"] = true
	prov.binds["immich"] = []ClassifiedBind{mandatoryHDD("appdata/immich"), optionalUserdata("media/photos")}
	_ = sett.SetAppOffbox("immich", true)

	cap := &backupCapture{}
	m.SetOffboxRunner(cap.runner())
	if err := m.RunOffboxBackup(context.Background()); err != nil {
		t.Fatalf("run: %v", err)
	}
	if cap.backups != 1 {
		t.Fatalf("exactly ONE snapshot per app, got %d backup calls", cap.backups)
	}
	args := cap.byStack["immich"]
	wantMandatory := mandAbs(drive, "appdata/immich")
	if !contains(args, unit) {
		t.Errorf("backup argv missing the unit path %q: %v", unit, args)
	}
	if !contains(args, wantMandatory) {
		t.Errorf("backup argv missing the mandatory userdata path %q: %v", wantMandatory, args)
	}
	if contains(args, mandAbs(drive, "userdata/media/photos")) {
		t.Errorf("OPTIONAL :ro path must NOT ship offsite: %v", args)
	}
}

// --- Scenario B: legacy / undeployed stay unit-only ---

func TestOffbox3a_LegacyUnitOnly(t *testing.T) {
	drive := t.TempDir()
	m, sett, prov := classifiedOffboxManager(t, drive)
	unit := mkUnit(t, drive, "sonarr")
	if err := os.MkdirAll(filepath.Join(drive, "appdata", "sonarr"), 0o755); err != nil {
		t.Fatal(err)
	}
	prov.hdd["sonarr"] = drive
	prov.has["sonarr"] = false // block REJECTED / absent → legacy (binds present but no class semantics)
	prov.binds["sonarr"] = []ClassifiedBind{mandatoryHDD("appdata/sonarr")}
	_ = sett.SetAppOffbox("sonarr", true)

	cap := &backupCapture{}
	m.SetOffboxRunner(cap.runner())
	if err := m.RunOffboxBackup(context.Background()); err != nil {
		t.Fatalf("run: %v", err)
	}
	args := cap.byStack["sonarr"]
	// unit-only: exactly the base shape, last arg is the unit, no extra resolved paths.
	if args[len(args)-1] != unit {
		t.Errorf("legacy app argv must END at the unit (no resolved paths), got %v", args)
	}
	for _, a := range args {
		if strings.Contains(a, "appdata") || strings.Contains(a, "userdata") {
			t.Errorf("legacy app resolved a bind into offsite argv (SQ5 regression): %v", args)
		}
	}
}

func TestOffbox3a_UndeployedUnitOnlyWithWarning(t *testing.T) {
	drive := t.TempDir()
	m, sett, prov := classifiedOffboxManager(t, drive)
	_ = mkUnit(t, drive, "immich")
	prov.hdd["immich"] = "" // undeployed → no live HDD_PATH
	prov.has["immich"] = true
	prov.binds["immich"] = []ClassifiedBind{mandatoryHDD("appdata/immich")}
	_ = sett.SetAppOffbox("immich", true)

	cap := &backupCapture{}
	m.SetOffboxRunner(cap.runner())
	if err := m.RunOffboxBackup(context.Background()); err != nil {
		t.Fatalf("run: %v", err)
	}
	args := cap.byStack["immich"]
	if strings.Contains(strings.Join(args, " "), "appdata") {
		t.Errorf("undeployed app must push unit-only: %v", args)
	}
	if w := sett.GetOffboxTarget().LastWarning; !strings.Contains(w, "nincs telepítve") {
		t.Errorf("undeployed warning missing from LastWarning: %q", w)
	}
}

// --- Scenario C: pre-push enlargement gate ---

func TestOffbox3a_EnlargementGateBlocks(t *testing.T) {
	drive := t.TempDir()
	m, sett, prov := classifiedOffboxManager(t, drive)
	for _, app := range []string{"immich", "small"} {
		_ = mkUnit(t, drive, app)
		if err := os.MkdirAll(filepath.Join(drive, "appdata", app), 0o755); err != nil {
			t.Fatal(err)
		}
		prov.hdd[app] = drive
		prov.has[app] = true
		prov.binds[app] = []ClassifiedBind{mandatoryHDD("appdata/" + app)}
		_ = sett.SetAppOffbox(app, true)
	}
	_ = sett.UpdateOffboxStatus(func(o *settings.OffboxTarget) { o.QuotaGB = 50; o.RepoSizeBytes = 20 << 30 })
	// immich's mandatory set is 40 GiB (20+40 ≥ 50 → blocked); small's is 1 GiB (20+1 < 50 → fits).
	m.SetOffboxSizer(func(p string) int64 {
		if strings.Contains(p, "immich") {
			return 40 << 30
		}
		return 1 << 30
	})
	var noteMu sync.Mutex
	var notes []string
	m.SetOffboxEnlargeBlockedNotifier(func(stack string, _ int64, usedGB, quotaGB int) {
		noteMu.Lock()
		defer noteMu.Unlock()
		notes = append(notes, stack)
		if usedGB != 20 || quotaGB != 50 {
			t.Errorf("notifier numbers wrong: used=%d quota=%d", usedGB, quotaGB)
		}
	})
	cap := &backupCapture{}
	m.SetOffboxRunner(cap.runner())
	if err := m.RunOffboxBackup(context.Background()); err != nil {
		t.Fatalf("run must be OK (a blocked enlargement is not a run failure): %v", err)
	}
	// immich → unit-only; small → enlarged.
	if strings.Contains(strings.Join(cap.byStack["immich"], " "), "appdata") {
		t.Errorf("blocked immich must be unit-only: %v", cap.byStack["immich"])
	}
	if !contains(cap.byStack["small"], mandAbs(drive, "appdata/small")) {
		t.Errorf("fitting 'small' must still push enlarged: %v", cap.byStack["small"])
	}
	tgt := sett.GetOffboxTarget()
	if len(tgt.EnlargedBlocked) != 1 || tgt.EnlargedBlocked[0] != "immich" {
		t.Errorf("EnlargedBlocked = %v, want [immich]", tgt.EnlargedBlocked)
	}
	if tgt.LastStatus != "ok" {
		t.Errorf("run status = %q, want ok", tgt.LastStatus)
	}
	if !strings.Contains(tgt.LastWarning, "tárhelykeret miatt") || !strings.Contains(tgt.LastWarning, "immich") {
		t.Errorf("blocked LastWarning missing: %q", tgt.LastWarning)
	}
	if len(notes) != 1 || notes[0] != "immich" {
		t.Errorf("notifier must fire ONCE for immich, got %v", notes)
	}

	// EnlargedBlocked clears on a subsequent run where nothing is blocked.
	m.SetOffboxSizer(func(string) int64 { return 1 << 30 }) // now immich fits too
	if err := m.RunOffboxBackup(context.Background()); err != nil {
		t.Fatal(err)
	}
	if b := sett.GetOffboxTarget().EnlargedBlocked; len(b) != 0 {
		t.Errorf("EnlargedBlocked must clear when nothing is blocked, got %v", b)
	}
}

// --- Scenario D: capture gaps are loud (SP-3.4) ---

func TestOffbox3a_CaptureGapsAreLoud(t *testing.T) {
	drive := t.TempDir()
	m, sett, prov := classifiedOffboxManager(t, drive)
	_ = mkUnit(t, drive, "app")
	if err := os.MkdirAll(filepath.Join(drive, "appdata", "good"), 0o755); err != nil {
		t.Fatal(err)
	}
	prov.hdd["app"] = drive
	prov.has["app"] = true
	prov.binds["app"] = []ClassifiedBind{
		mandatoryHDD("appdata/good"),  // exists → captured
		mandatoryHDD("../evil"),       // D1: traversal → Skipped
		mandatoryHDD("appdata/ghost"), // D2: passes guards but absent on disk → stat-filtered
	}
	_ = sett.SetAppOffbox("app", true)

	cap := &backupCapture{}
	m.SetOffboxRunner(cap.runner())
	if err := m.RunOffboxBackup(context.Background()); err != nil {
		t.Fatalf("run: %v", err)
	}
	args := cap.byStack["app"]
	joined := strings.Join(args, " ")
	if !contains(args, mandAbs(drive, "appdata/good")) {
		t.Errorf("the valid mandatory path must still push: %v", args)
	}
	if strings.Contains(joined, "evil") {
		t.Errorf("traversal path escaped into argv: %v", args)
	}
	if strings.Contains(joined, "ghost") {
		t.Errorf("stat-missing mandatory path must NOT be in argv (SP-3.4 silent-skip): %v", args)
	}
	if w := sett.GetOffboxTarget().LastWarning; !strings.Contains(w, "nem kerültek a távoli mentésbe") {
		t.Errorf("capture-gap warning missing from LastWarning: %q", w)
	}
}

// --- §8 all-excluded row (radarr shape): unit-only, NO warning ---

func TestOffbox3a_AllExcludedUnitOnlyNoWarning(t *testing.T) {
	drive := t.TempDir()
	m, sett, prov := classifiedOffboxManager(t, drive)
	unit := mkUnit(t, drive, "radarr")
	prov.hdd["radarr"] = drive
	prov.has["radarr"] = true
	prov.binds["radarr"] = []ClassifiedBind{excludedHDD("appdata/radarr"), excludedHDD("downloads")}
	_ = sett.SetAppOffbox("radarr", true)

	cap := &backupCapture{}
	m.SetOffboxRunner(cap.runner())
	if err := m.RunOffboxBackup(context.Background()); err != nil {
		t.Fatalf("run: %v", err)
	}
	if args := cap.byStack["radarr"]; args[len(args)-1] != unit {
		t.Errorf("all-excluded app must be unit-only: %v", args)
	}
	if w := sett.GetOffboxTarget().LastWarning; strings.Contains(w, "nem kerültek") {
		t.Errorf("all-excluded is correct, NOT a gap — no warning expected, got %q", w)
	}
}

// --- Scenario E: raw-data stats mode ---

func TestOffbox3a_StatsRawDataMode(t *testing.T) {
	m, sett := newOffboxManager(t)
	var statsArgs []string
	m.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) {
		switch {
		case contains(args, "snapshots"):
			return []byte(`[{"id":"a"}]`), nil
		case contains(args, "stats"):
			statsArgs = append([]string{}, args...)
			return []byte(`{"total_size":987654321}`), nil
		}
		return nil, nil
	})
	base, env := m.offboxBaseArgs(sett.GetOffboxTarget())
	m.offboxRecordStats(context.Background(), base, env)
	if !contains(statsArgs, "--mode") || valAfter(statsArgs, "--mode") != "raw-data" {
		t.Fatalf("stats must run in raw-data mode, got %v", statsArgs)
	}
	if got := sett.GetOffboxTarget().RepoSizeBytes; got != 987654321 {
		t.Errorf("RepoSizeBytes = %d, want 987654321 (parsed from raw-data total_size)", got)
	}
}

// --- Scenario F: both forget call sites carry --group-by host,tags ---

func TestOffbox3a_ForgetGrouping_MainRun(t *testing.T) {
	drive := t.TempDir()
	m, sett, prov := classifiedOffboxManager(t, drive)
	_ = mkUnit(t, drive, "app")
	prov.has["app"] = false
	_ = sett.SetAppOffbox("app", true)
	cap := &backupCapture{}
	m.SetOffboxRunner(cap.runner())
	if err := m.RunOffboxBackup(context.Background()); err != nil {
		t.Fatal(err)
	}
	if len(cap.forgets) != 1 {
		t.Fatalf("expected one forget call, got %d", len(cap.forgets))
	}
	if valAfter(cap.forgets[0], "--group-by") != "host,tags" {
		t.Errorf("main-run forget missing --group-by host,tags: %v", cap.forgets[0])
	}
}

func TestOffbox3a_ForgetGrouping_OverQuotaPrune(t *testing.T) {
	m, sett := newOffboxManager(t)
	_ = sett.UpdateOffboxStatus(func(o *settings.OffboxTarget) { o.QuotaGB = 50; o.RepoSizeBytes = 51 << 30 })
	var forgetArgs []string
	m.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) {
		switch {
		case contains(args, "cat") && contains(args, "config"):
			return []byte(`{}`), nil
		case contains(args, "forget"):
			forgetArgs = append([]string{}, args...)
		case contains(args, "snapshots"):
			return []byte(`[]`), nil
		case contains(args, "stats"):
			return []byte(`{"total_size":1}`), nil
		}
		return nil, nil
	})
	_ = m.RunOffboxBackup(context.Background()) // over-quota → prune-only path
	if valAfter(forgetArgs, "--group-by") != "host,tags" {
		t.Errorf("over-quota prune forget missing --group-by host,tags: %v", forgetArgs)
	}
}

// --- Scenario E-restore: unit-only restore argv (ID-first + --include) + scratch OFF the rootfs ---

func TestOffbox3a_UnitOnlyRestoreArgv(t *testing.T) {
	drive := t.TempDir()
	m, _, prov := classifiedOffboxManager(t, drive)
	prov.hdd["immich"] = drive
	m.SetOffboxFreeFn(func(string) int64 { return 100 << 30 }) // plenty
	unitPath := filepath.ToSlash(filepath.Join(drive, "backups", "primary", "immich"))
	var restoreArgs []string
	m.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) {
		switch {
		case contains(args, "snapshots"):
			return []byte(`[{"short_id":"deadbeef","time":"2026-07-14T00:00:00Z","paths":["` + filepath.ToSlash(filepath.Join(drive, "appdata", "immich")) + `","` + unitPath + `"]}]`), nil
		case contains(args, "restore"):
			restoreArgs = append([]string{}, args...)
		}
		return nil, nil
	})
	if err := m.RestoreOffboxScratch(context.Background(), "immich", false); err != nil {
		t.Fatalf("unit-only restore: %v", err)
	}
	if valAfter(restoreArgs, "restore") != "deadbeef" {
		t.Errorf("restore must be ID-first (deadbeef): %v", restoreArgs)
	}
	if valAfter(restoreArgs, "--include") != unitPath {
		t.Errorf("unit-only restore must --include the absolute unit path %q: %v", unitPath, restoreArgs)
	}
	target := valAfter(restoreArgs, "--target")
	if !strings.HasPrefix(target, drive) || strings.Contains(target, m.cfg.Paths.DataDir) {
		t.Errorf("scratch target must be on the data drive, never DataDir: %q", target)
	}
}

// full restore refuses fail-closed when the snapshot size is unknown (no restore call made).
func TestOffbox3a_FullRestoreRefusesOnSizeUnknown(t *testing.T) {
	drive := t.TempDir()
	m, _, prov := classifiedOffboxManager(t, drive)
	prov.hdd["immich"] = drive
	m.SetOffboxFreeFn(func(string) int64 { return 100 << 30 })
	unitPath := filepath.Join(drive, "backups", "primary", "immich")
	restoreCalled := false
	m.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) {
		switch {
		case contains(args, "snapshots"):
			return []byte(`[{"short_id":"a","time":"2026-07-14T00:00:00Z","paths":["` + filepath.ToSlash(unitPath) + `"]}]`), nil
		case contains(args, "stats"):
			return nil, context.DeadlineExceeded // size lookup fails → unknown
		case contains(args, "restore"):
			restoreCalled = true
		}
		return nil, nil
	})
	err := m.RestoreOffboxScratch(context.Background(), "immich", true)
	if err == nil || !strings.Contains(err.Error(), "nem állapítható meg") {
		t.Fatalf("full restore must refuse fail-closed on unknown size, got err=%v", err)
	}
	if restoreCalled {
		t.Error("no restic restore call may run when the size is unknown")
	}
}

// old rootfs scratch is cleaned up on a new restore.
func TestOffbox3a_LegacyRootfsScratchCleanup(t *testing.T) {
	drive := t.TempDir()
	m, _, prov := classifiedOffboxManager(t, drive)
	prov.hdd["immich"] = drive
	m.SetOffboxFreeFn(func(string) int64 { return 100 << 30 })
	legacy := filepath.Join(m.cfg.Paths.DataDir, "offbox-restore", "immich")
	if err := os.MkdirAll(legacy, 0o755); err != nil {
		t.Fatal(err)
	}
	unitPath := filepath.Join(drive, "backups", "primary", "immich")
	m.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) {
		if contains(args, "snapshots") {
			return []byte(`[{"short_id":"a","time":"2026-07-14T00:00:00Z","paths":["` + filepath.ToSlash(unitPath) + `"]}]`), nil
		}
		return nil, nil
	})
	if err := m.RestoreOffboxScratch(context.Background(), "immich", false); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(legacy); !os.IsNotExist(err) {
		t.Errorf("legacy rootfs scratch %s must be removed, stat err=%v", legacy, err)
	}
}

// --- Scenario G: place-to-live mapping (pure) + the wrong cases ---

func TestMapOffsiteRestorePaths(t *testing.T) {
	old := "/old/ns"
	newNs := "/new/ns"
	scratch := "/scratch"
	snap := []string{
		old + "/backups/primary/app",
		old + "/appdata/app",
		old + "/userdata/media/x",
	}
	got, err := mapOffsiteRestorePaths(snap, "app", scratch, newNs)
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if len(got) != 3 {
		t.Fatalf("got %d placements, want 3: %+v", len(got), got)
	}
	byDst := map[string]placement{}
	for _, pl := range got {
		byDst[pl.dst] = pl
	}
	// anchor derived by trimming backups/primary/app off the unit path → oldNs; dst = newNs/<rel>,
	// src = scratch/<abs-source> (SP-3.1). Built with filepath.Join to match the code (OS separators).
	check := func(snapPath, rel string, isUnit bool) {
		dst := filepath.Join(newNs, rel)
		pl, ok := byDst[dst]
		if !ok {
			t.Errorf("missing placement for dst %q", dst)
			return
		}
		if pl.src != filepath.Join(scratch, snapPath) {
			t.Errorf("src for %q = %q, want %q", snapPath, pl.src, filepath.Join(scratch, snapPath))
		}
		if pl.isUnit != isUnit {
			t.Errorf("isUnit for %q = %v, want %v", snapPath, pl.isUnit, isUnit)
		}
	}
	check(old+"/backups/primary/app", "backups/primary/app", true)
	check(old+"/appdata/app", "appdata/app", false)
	check(old+"/userdata/media/x", "userdata/media/x", false)

	// Wrong cases — each REFUSES the whole placement.
	if _, err := mapOffsiteRestorePaths([]string{old + "/appdata/app"}, "app", scratch, newNs); err == nil {
		t.Error("no unit path → must refuse")
	}
	if _, err := mapOffsiteRestorePaths([]string{old + "/backups/primary/app", "/elsewhere/x"}, "app", scratch, newNs); err == nil {
		t.Error("a path outside the namespace → must refuse")
	}
	if _, err := mapOffsiteRestorePaths([]string{old + "/backups/primary/app", old + "/backups/secondary/y"}, "app", scratch, newNs); err == nil {
		t.Error("a non-unit path in the reserved backups/ zone → must refuse")
	}
}
