// Copyright 2025 Emiliano Spinella (eminwux)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package cgroups_test

import (
	"bytes"
	"context"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"syscall"
	"testing"

	cgroupscmd "github.com/eminwux/kukeon/cmd/kuke/doctor/cgroups"
	kukshared "github.com/eminwux/kukeon/cmd/kuke/shared"
	"github.com/eminwux/kukeon/internal/errdefs"
	"github.com/eminwux/kukeon/pkg/api/kukeonv1"
	v1beta1 "github.com/eminwux/kukeon/pkg/api/model/v1beta1"
)

// TestMain mocks the shared euid lookup to euid=0 and neutralizes the
// host-environment probes (swap + userspace OOM guard) for every test in
// this package. The fail-fast root gate on --probe would otherwise
// short-circuit every default-probe and --probe test under non-root CI
// runners (ubuntu-latest defaults to UID 1001), even though their fake
// cgroupfs lives under t.TempDir() and never needs real root. The host
// probes default to no-op returns so existing tests do not depend on the
// runner's actual /proc/swaps or running-process set; individual probe
// tests below override these with their own SetHostProbesForTest call.
func TestMain(m *testing.M) {
	restoreEuid := kukshared.SetGeteuidForTesting(func() int { return 0 })
	restoreProbes := cgroupscmd.SetHostProbesForTest(
		func() string { return "" },
		func() string { return "" },
	)
	code := m.Run()
	restoreProbes()
	restoreEuid()
	os.Exit(code)
}

// writeFakeCgroup materializes a directory pretending to be a cgroup-v2
// root: cgroup.controllers + cgroup.subtree_control with the given
// space-separated controller lists.
func writeFakeCgroup(t *testing.T, controllers, subtree string) string {
	t.Helper()
	root := t.TempDir()
	writeFakeCgroupAt(t, root, controllers, subtree)
	return root
}

// writeFakeCgroupAt is like writeFakeCgroup but writes at a caller-supplied
// directory so scoped tests can build a host-root + nested-cgroup-dir
// hierarchy in a single TempDir.
func writeFakeCgroupAt(t *testing.T, dir, controllers, subtree string) {
	t.Helper()
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatalf("mkdir %s: %v", dir, err)
	}
	if err := os.WriteFile(filepath.Join(dir, "cgroup.controllers"), []byte(controllers), 0o644); err != nil {
		t.Fatalf("write cgroup.controllers: %v", err)
	}
	if err := os.WriteFile(filepath.Join(dir, "cgroup.subtree_control"), []byte(subtree), 0o644); err != nil {
		t.Fatalf("write cgroup.subtree_control: %v", err)
	}
}

func runCmd(t *testing.T, args ...string) (string, string, error) {
	t.Helper()
	return runCmdWithClient(t, nil, args...)
}

// runCmdWithClient invokes the cgroups command with an optional fake
// kukeonv1.Client injected via the MockControllerKey context value. nil
// client means "no scope; the host-root path is taken".
func runCmdWithClient(t *testing.T, client kukeonv1.Client, args ...string) (string, string, error) {
	t.Helper()
	cmd := cgroupscmd.NewCgroupsCmd()
	stdout := &bytes.Buffer{}
	stderr := &bytes.Buffer{}
	cmd.SetOut(stdout)
	cmd.SetErr(stderr)
	cmd.SetArgs(args)
	ctx := context.Background()
	if client != nil {
		ctx = context.WithValue(ctx, cgroupscmd.MockControllerKey{}, client)
	}
	cmd.SetContext(ctx)
	err := cmd.Execute()
	return stdout.String(), stderr.String(), err
}

// TestCgroupsCmdHappyPath: when every required controller is enabled, the
// command exits 0 and prints nothing to stdout. The dev-init.sh tail
// regression guard relies on this silence.
func TestCgroupsCmdHappyPath(t *testing.T) {
	root := writeFakeCgroup(t,
		"cpuset cpu io memory hugetlb pids rdma misc",
		"cpu memory io pids",
	)

	stdout, stderr, err := runCmd(t, "--root", root)
	if err != nil {
		t.Fatalf("happy-path Execute() error = %v\nstdout=%q\nstderr=%q", err, stdout, stderr)
	}
	if stdout != "" {
		t.Errorf("happy-path stdout = %q, want empty (silence keeps dev-init.sh tail clean)", stdout)
	}
}

// TestCgroupsCmdMissingFailsNoProbe: with --no-probe, when memory + io are
// missing from subtree_control, the command exits non-zero and the stderr
// remediation names them and the canonical fix. Pinning --no-probe rather
// than the bare default keeps the assertion stable now that probing is
// the default behavior — the tempdir-backed subtree_control would otherwise
// accept the +<ctrl> writes and the pre-flight would pass.
func TestCgroupsCmdMissingFailsNoProbe(t *testing.T) {
	root := writeFakeCgroup(t,
		"cpuset cpu io memory pids",
		"cpu pids",
	)

	_, stderr, err := runCmd(t, "--root", root, "--no-probe")
	if err == nil {
		t.Fatal("Execute() error = nil for host missing memory + io with --no-probe, want error")
	}

	for _, want := range []string{"memory", "io", "cgroup.subtree_control"} {
		if !strings.Contains(stderr, want) {
			t.Errorf("stderr missing %q:\n%s", want, stderr)
		}
	}
}

// TestCgroupsCmdProbesByDefault: a bare `kuke doctor cgroups` (no --probe,
// no --no-probe) must probe by default — that is the fix #333 calls for.
// On the tempdir-backed fake cgroup the +<ctrl> writes succeed, so missing
// controllers end up StatusEnabledByProbe and the pre-flight exits 0.
// This pins the new default so a future regression that re-disables probe
// fails the test instead of silently regressing the user-visible behavior.
func TestCgroupsCmdProbesByDefault(t *testing.T) {
	root := writeFakeCgroup(t,
		"cpuset cpu io memory pids",
		"cpu pids",
	)

	stdout, stderr, err := runCmd(t, "--root", root)
	if err != nil {
		t.Fatalf("default-probe Execute() error = %v\nstdout=%q\nstderr=%q", err, stdout, stderr)
	}
	if stdout != "" {
		t.Errorf("default-probe stdout = %q, want empty (silence on probe success)", stdout)
	}
}

// TestCgroupsCmdProbeRecoversOnPlainFile: --probe writes "+<ctrl>" to the
// fake subtree_control file. After the write the controllers are present
// in the file and a re-read inside cgroupcheck classifies them as
// EnabledByProbe → command returns success. Exercises the same probe
// path dev-init.sh uses on a misconfigured host. Pinning the explicit
// --probe form keeps the dev-init.sh callsite contractually covered even
// after probe-by-default became the new default.
func TestCgroupsCmdProbeRecoversOnPlainFile(t *testing.T) {
	root := writeFakeCgroup(t,
		"cpuset cpu io memory pids",
		"cpu pids",
	)

	stdout, stderr, err := runCmd(t, "--root", root, "--probe")
	if err != nil {
		t.Fatalf("probe-recover Execute() error = %v\nstdout=%q\nstderr=%q", err, stdout, stderr)
	}
}

// TestCgroupsCmdNoProbeWinsOverProbe: when both --probe and --no-probe are
// passed, --no-probe wins and the pre-flight stays read-only. Pins the
// "explicit opt-out is unambiguous regardless of flag ordering" contract
// so a future change can't silently re-enable probing for an operator who
// asked for read-only.
func TestCgroupsCmdNoProbeWinsOverProbe(t *testing.T) {
	root := writeFakeCgroup(t,
		"cpuset cpu io memory pids",
		"cpu pids",
	)

	_, stderr, err := runCmd(t, "--root", root, "--probe", "--no-probe")
	if err == nil {
		t.Fatal("Execute() error = nil with --probe --no-probe and missing controllers, want error")
	}
	for _, want := range []string{"memory", "io"} {
		if !strings.Contains(stderr, want) {
			t.Errorf("stderr missing %q under --probe --no-probe:\n%s", want, stderr)
		}
	}
}

// TestCgroupsCmdNestedReadsAdvertised: --nested-cgroup-runtime requires
// the full host-advertised set. Seeding subtree_control with the full
// set must therefore pass the pre-flight (no second source of truth).
func TestCgroupsCmdNestedReadsAdvertised(t *testing.T) {
	full := "cpuset cpu io memory hugetlb pids rdma misc"
	root := writeFakeCgroup(t, full, full)

	if _, stderr, err := runCmd(t, "--root", root, "--nested-cgroup-runtime"); err != nil {
		t.Fatalf("nested happy-path failed: %v\nstderr=%q", err, stderr)
	}
}

// TestCgroupsCmdNestedFailsOnPartialSubtree: with --nested-cgroup-runtime,
// any host-advertised controller missing from subtree_control must fail
// the pre-flight. Confirms the "moving target" handling: when the cell
// adopts NestedCgroupRuntime the required set widens automatically.
func TestCgroupsCmdNestedFailsOnPartialSubtree(t *testing.T) {
	full := "cpuset cpu io memory hugetlb pids rdma misc"
	root := writeFakeCgroup(t, full, "cpu memory io pids")

	_, stderr, err := runCmd(t, "--root", root, "--nested-cgroup-runtime", "--no-probe")
	if err == nil {
		t.Fatal("nested partial-subtree Execute() error = nil, want error")
	}
	for _, want := range []string{"cpuset", "hugetlb", "rdma", "misc"} {
		if !strings.Contains(stderr, want) {
			t.Errorf("stderr missing %q under --nested-cgroup-runtime:\n%s", want, stderr)
		}
	}
}

func TestCgroupsCmdNoSuchRoot(t *testing.T) {
	bogus := filepath.Join(t.TempDir(), "no-such-root")
	_, stderr, err := runCmd(t, "--root", bogus)
	if err == nil {
		t.Fatal("Execute() error = nil for missing host root, want error")
	}
	if !strings.Contains(err.Error(), "cgroup pre-flight") {
		t.Errorf("error message = %q, want it to mention 'cgroup pre-flight'", err.Error())
	}
	_ = stderr
}

// TestCgroupsCmdSelfHealDowngradeOnInternalProcess: when the only
// unresolved class is the EBUSY no-internal-process trap (i.e. what
// applySubtreeControllers's defensive __bootstrap drain self-heals at
// `kuke init` time, PR #340) AND the host fingerprint matches both
// defensive-drain clauses, the pre-flight surfaces the diagnostic on
// stderr but exits 0 so dev-init.sh proceeds. This is the issue #342
// repro: stop the doctor from rejecting hosts the runtime would heal.
func TestCgroupsCmdSelfHealDowngradeOnInternalProcess(t *testing.T) {
	root := writeFakeCgroup(t,
		"cpuset cpu io memory pids",
		"cpu pids",
	)
	// Inject a prober that returns EBUSY for memory/io (the
	// no-internal-process trap) so the result lands in
	// StatusInternalProcess for those controllers.
	restoreProber := cgroupscmd.SetActiveProberForTest(func(_, ctrl string) error {
		if ctrl == "memory" || ctrl == "io" {
			return syscall.EBUSY
		}
		return nil
	})
	defer restoreProber()
	// Force the gate to match (production reads /proc/self/cgroup +
	// cgroup.events; tests inject directly).
	restoreGate := cgroupscmd.SetSelfHealGateForTest(func(string) bool { return true })
	defer restoreGate()

	stdout, stderr, err := runCmd(t, "--root", root)
	if err != nil {
		t.Fatalf("self-heal Execute() error = %v\nstdout=%q\nstderr=%q", err, stdout, stderr)
	}
	if stdout != "" {
		t.Errorf("self-heal stdout = %q, want empty (all output goes to stderr)", stdout)
	}
	// Diagnostic is still surfaced — operators must see what is about
	// to happen.
	for _, want := range []string{
		"internal-process",
		"no-internal-process rule",
		"memory, io",
		"applySubtreeControllers",
		"__bootstrap",
		"`kuke init` will proceed",
	} {
		if !strings.Contains(stderr, want) {
			t.Errorf("self-heal stderr missing %q:\n%s", want, stderr)
		}
	}
}

// TestCgroupsCmdSelfHealNoDowngradeWithoutGate: if the
// no-internal-process trap fires but the host fingerprint does NOT
// match (gate returns false), the runtime drain would also not run, so
// the pre-flight must remain fatal. Pins the gate as load-bearing —
// without both clauses holding, the downgrade can't fire.
func TestCgroupsCmdSelfHealNoDowngradeWithoutGate(t *testing.T) {
	root := writeFakeCgroup(t,
		"cpuset cpu io memory pids",
		"cpu pids",
	)
	restoreProber := cgroupscmd.SetActiveProberForTest(func(_, ctrl string) error {
		if ctrl == "memory" || ctrl == "io" {
			return syscall.EBUSY
		}
		return nil
	})
	defer restoreProber()
	restoreGate := cgroupscmd.SetSelfHealGateForTest(func(string) bool { return false })
	defer restoreGate()

	_, stderr, err := runCmd(t, "--root", root)
	if err == nil {
		t.Fatal("Execute() error = nil with EBUSY trap and gate=false, want fatal")
	}
	for _, want := range []string{"internal-process", "no-internal-process rule"} {
		if !strings.Contains(stderr, want) {
			t.Errorf("stderr missing %q:\n%s", want, stderr)
		}
	}
	// The "self-heal" note must NOT be printed when the gate is false —
	// the operator would otherwise be told `kuke init` will proceed
	// when in fact it would EBUSY.
	if strings.Contains(stderr, "`kuke init` will proceed") {
		t.Errorf("self-heal note printed despite gate=false:\n%s", stderr)
	}
}

// TestCgroupsCmdSelfHealNoDowngradeOnMixedClasses: the downgrade only
// fires when EVERY unresolved controller is StatusInternalProcess. When
// one controller is internal-process and another is kernel-missing,
// the runtime drain cannot rescue the second, so the pre-flight must
// remain fatal. Pins the OnlyInternalProcess boundary so a future
// change can't silently downgrade mixed-class failures.
func TestCgroupsCmdSelfHealNoDowngradeOnMixedClasses(t *testing.T) {
	// memory advertised, io NOT advertised → io stays
	// StatusKernelMissing regardless of probe; memory EBUSYs to
	// StatusInternalProcess. Mixed → must remain fatal even with the
	// gate matching.
	root := writeFakeCgroup(t,
		"cpuset cpu memory pids",
		"cpu pids",
	)
	restoreProber := cgroupscmd.SetActiveProberForTest(func(_, ctrl string) error {
		if ctrl == "memory" {
			return syscall.EBUSY
		}
		return nil
	})
	defer restoreProber()
	restoreGate := cgroupscmd.SetSelfHealGateForTest(func(string) bool { return true })
	defer restoreGate()

	_, stderr, err := runCmd(t, "--root", root)
	if err == nil {
		t.Fatal("Execute() error = nil on mixed internal-process + kernel-missing, want fatal")
	}
	if !strings.Contains(stderr, "kernel does not support") {
		t.Errorf("stderr missing kernel-missing stanza on mixed-class failure:\n%s", stderr)
	}
	if strings.Contains(stderr, "`kuke init` will proceed") {
		t.Errorf("self-heal note printed despite kernel-missing controllers in the same result:\n%s", stderr)
	}
}

// TestCgroupsCmdSelfHealNoDowngradeOnScoped: --scope paths probe a
// sub-tree, not the cgroup-namespace mountpoint root. The runtime
// drain in applySubtreeControllers only operates on the mountpoint
// root, so a scoped EBUSY is genuinely operator-actionable and must
// stay fatal even when the gate matches.
func TestCgroupsCmdSelfHealNoDowngradeOnScoped(t *testing.T) {
	root := t.TempDir()
	writeFakeCgroupAt(t, root,
		"cpuset cpu io memory pids",
		"cpu memory io pids",
	)
	realmCg := "/kukeon/default"
	writeFakeCgroupAt(t, filepath.Join(root, realmCg),
		"cpuset cpu io memory pids",
		"cpu pids",
	)
	restoreProber := cgroupscmd.SetActiveProberForTest(func(_, ctrl string) error {
		if ctrl == "memory" || ctrl == "io" {
			return syscall.EBUSY
		}
		return nil
	})
	defer restoreProber()
	restoreGate := cgroupscmd.SetSelfHealGateForTest(func(string) bool { return true })
	defer restoreGate()

	client := &fakeScopedClient{realmExists: true, realmCgroupPath: realmCg}
	_, stderr, err := runCmdWithClient(t, client,
		"--scope", "realm", "default", "--root", root,
	)
	if err == nil {
		t.Fatal("scope=realm Execute() error = nil with EBUSY at scoped path, want fatal")
	}
	if strings.Contains(stderr, "`kuke init` will proceed") {
		t.Errorf("self-heal note printed for scoped pre-flight (runtime drain does not heal scoped EBUSY):\n%s", stderr)
	}
}

// fakeScopedClient embeds FakeClient so only the Get* methods relevant to
// each --scope test are overridden. CgroupPath is the value the daemon
// would have populated on Status; tests join it with --root to point the
// pre-flight at a synthetic cgroup directory.
type fakeScopedClient struct {
	kukeonv1.FakeClient

	realmCgroupPath string
	spaceCgroupPath string
	stackCgroupPath string
	cellCgroupPath  string

	// realm/space/stack/cell metadata gates so tests can simulate "not
	// found" without overriding all four methods.
	realmExists bool
	spaceExists bool
	stackExists bool
	cellExists  bool
}

func (f *fakeScopedClient) GetRealm(_ context.Context, doc v1beta1.RealmDoc) (kukeonv1.GetRealmResult, error) {
	out := v1beta1.RealmDoc{
		Metadata: doc.Metadata,
		Status:   v1beta1.RealmStatus{CgroupPath: f.realmCgroupPath},
	}
	return kukeonv1.GetRealmResult{
		Realm:          out,
		MetadataExists: f.realmExists,
	}, nil
}

func (f *fakeScopedClient) GetSpace(_ context.Context, doc v1beta1.SpaceDoc) (kukeonv1.GetSpaceResult, error) {
	out := v1beta1.SpaceDoc{
		Metadata: doc.Metadata,
		Spec:     doc.Spec,
		Status:   v1beta1.SpaceStatus{CgroupPath: f.spaceCgroupPath},
	}
	return kukeonv1.GetSpaceResult{
		Space:          out,
		MetadataExists: f.spaceExists,
	}, nil
}

func (f *fakeScopedClient) GetStack(_ context.Context, doc v1beta1.StackDoc) (kukeonv1.GetStackResult, error) {
	out := v1beta1.StackDoc{
		Metadata: doc.Metadata,
		Spec:     doc.Spec,
		Status:   v1beta1.StackStatus{CgroupPath: f.stackCgroupPath},
	}
	return kukeonv1.GetStackResult{
		Stack:          out,
		MetadataExists: f.stackExists,
	}, nil
}

func (f *fakeScopedClient) GetCell(_ context.Context, doc v1beta1.CellDoc) (kukeonv1.GetCellResult, error) {
	out := v1beta1.CellDoc{
		Metadata: doc.Metadata,
		Spec:     doc.Spec,
		Status:   v1beta1.CellStatus{CgroupPath: f.cellCgroupPath},
	}
	return kukeonv1.GetCellResult{
		Cell:           out,
		MetadataExists: f.cellExists,
	}, nil
}

// TestCgroupsCmdScopeResolvesAndPasses exercises every --scope kind
// against a synthetic cgroup-v2 hierarchy under one TempDir: the host
// root has the kukeond resource subset, and each named scope has the
// same set fully delegated, so the pre-flight must exit 0 for all four
// scope kinds. This is the resolution-path coverage AC #5 calls for.
func TestCgroupsCmdScopeResolvesAndPasses(t *testing.T) {
	root := t.TempDir()
	// Host-root files keep the unscoped path callable too (defensive —
	// the scoped path joins root with the cgroup-relative path).
	writeFakeCgroupAt(t, root,
		"cpuset cpu io memory hugetlb pids rdma misc",
		"cpu memory io pids",
	)
	// Synthetic per-scope directories. Cell sits under stack sits under
	// space sits under realm sits under root, mirroring real-world
	// /sys/fs/cgroup/kukeon/<realm>/<space>/<stack>/<cell> layout.
	full := "cpuset cpu io memory hugetlb pids rdma misc"
	enabled := "cpu memory io pids"
	realmCg := "/kukeon/default"
	spaceCg := "/kukeon/default/sp"
	stackCg := "/kukeon/default/sp/st"
	cellCg := "/kukeon/default/sp/st/ce"
	for _, p := range []string{realmCg, spaceCg, stackCg, cellCg} {
		writeFakeCgroupAt(t, filepath.Join(root, p), full, enabled)
	}

	client := &fakeScopedClient{
		realmExists: true, realmCgroupPath: realmCg,
		spaceExists: true, spaceCgroupPath: spaceCg,
		stackExists: true, stackCgroupPath: stackCg,
		cellExists: true, cellCgroupPath: cellCg,
	}

	cases := []struct {
		name string
		args []string
	}{
		{"realm", []string{"--scope", "realm", "default", "--root", root}},
		{"space", []string{"--scope", "space", "sp", "--realm", "default", "--root", root}},
		{"stack", []string{"--scope", "stack", "st", "--realm", "default", "--space", "sp", "--root", root}},
		{"cell", []string{
			"--scope", "cell", "ce",
			"--realm", "default", "--space", "sp", "--stack", "st",
			"--root", root,
		}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			stdout, stderr, err := runCmdWithClient(t, client, tc.args...)
			if err != nil {
				t.Fatalf("scope=%s Execute() error = %v\nstdout=%q\nstderr=%q",
					tc.name, err, stdout, stderr)
			}
			if stdout != "" {
				t.Errorf("scope=%s stdout = %q, want empty on success", tc.name, stdout)
			}
		})
	}
}

// TestCgroupsCmdScopeFailsOnGap: with a synthetic gap (memory missing
// from the realm's cgroup.subtree_control), --scope realm must exit
// non-zero and the stderr must identify the realm via the new "scope:"
// header line so the operator can tell which sub-tree failed. This is
// the gap-exit coverage AC #5 calls for.
func TestCgroupsCmdScopeFailsOnGap(t *testing.T) {
	root := t.TempDir()
	writeFakeCgroupAt(t, root,
		"cpuset cpu io memory hugetlb pids rdma misc",
		"cpu memory io pids",
	)
	realmCg := "/kukeon/default"
	// Gap: memory + io present in cgroup.controllers but not delegated
	// onto subtree_control at the realm level.
	writeFakeCgroupAt(t, filepath.Join(root, realmCg),
		"cpuset cpu io memory pids",
		"cpu pids",
	)

	client := &fakeScopedClient{realmExists: true, realmCgroupPath: realmCg}

	_, stderr, err := runCmdWithClient(t, client,
		"--scope", "realm", "default", "--root", root, "--no-probe",
	)
	if err == nil {
		t.Fatal("scope=realm gap Execute() error = nil, want error")
	}
	for _, want := range []string{`scope: realm "default"`, "memory", "io", "cgroup.subtree_control"} {
		if !strings.Contains(stderr, want) {
			t.Errorf("stderr missing %q:\n%s", want, stderr)
		}
	}
}

// TestCgroupsCmdScopeRequiresName: every --scope kind requires a
// positional <name>. The error must say so and not crash inside the
// daemon path.
func TestCgroupsCmdScopeRequiresName(t *testing.T) {
	client := &fakeScopedClient{}
	_, _, err := runCmdWithClient(t, client, "--scope", "realm")
	if err == nil {
		t.Fatal("Execute() error = nil for --scope realm without name, want error")
	}
	if !strings.Contains(err.Error(), "<name>") {
		t.Errorf("error = %q, want it to mention <name>", err.Error())
	}
}

// TestCgroupsCmdScopeRequiresParentFlags: --scope space|stack|cell each
// require their parent flags. Mirrors the same enforcement in
// `kuke get space|stack|cell` so operators learn one rule.
func TestCgroupsCmdScopeRequiresParentFlags(t *testing.T) {
	client := &fakeScopedClient{}
	cases := []struct {
		name    string
		args    []string
		wantSub string
	}{
		{"space-no-realm", []string{"--scope", "space", "sp"}, "--realm"},
		{"stack-no-realm", []string{"--scope", "stack", "st", "--space", "sp"}, "--realm"},
		{"stack-no-space", []string{"--scope", "stack", "st", "--realm", "r"}, "--space"},
		{"cell-no-realm", []string{"--scope", "cell", "c", "--space", "sp", "--stack", "st"}, "--realm"},
		{"cell-no-space", []string{"--scope", "cell", "c", "--realm", "r", "--stack", "st"}, "--space"},
		{"cell-no-stack", []string{"--scope", "cell", "c", "--realm", "r", "--space", "sp"}, "--stack"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, _, err := runCmdWithClient(t, client, tc.args...)
			if err == nil {
				t.Fatalf("Execute() error = nil for %s, want error", tc.name)
			}
			if !strings.Contains(err.Error(), tc.wantSub) {
				t.Errorf("error = %q, want it to mention %q", err.Error(), tc.wantSub)
			}
		})
	}
}

// TestCgroupsCmdScopeRejectsUnknownKind: an unknown --scope value must
// fail with a clear "must be one of" message and not silently fall
// through to the host-root path.
func TestCgroupsCmdScopeRejectsUnknownKind(t *testing.T) {
	client := &fakeScopedClient{}
	_, _, err := runCmdWithClient(t, client, "--scope", "container", "x")
	if err == nil {
		t.Fatal("Execute() error = nil for unknown scope, want error")
	}
	if !strings.Contains(err.Error(), "must be one of") {
		t.Errorf("error = %q, want it to mention 'must be one of'", err.Error())
	}
}

// TestCgroupsCmdScopeNotFound: when the daemon does not know the named
// resource, the scoped pre-flight must surface that (and not crash)
// before reading any cgroup file.
func TestCgroupsCmdScopeNotFound(t *testing.T) {
	root := t.TempDir()
	writeFakeCgroupAt(t, root,
		"cpuset cpu io memory hugetlb pids rdma misc",
		"cpu memory io pids",
	)
	client := &fakeScopedClient{realmExists: false}

	_, _, err := runCmdWithClient(t, client,
		"--scope", "realm", "ghost", "--root", root,
	)
	if err == nil {
		t.Fatal("Execute() error = nil for unknown realm, want error")
	}
	if !strings.Contains(err.Error(), "ghost") {
		t.Errorf("error = %q, want it to name the missing realm", err.Error())
	}
}

// TestCgroupsCmdProbeRequiresRoot pins the fail-fast UID gate that fires
// only when probe is effective. The probe writes "+<ctrl>" to
// cgroup.subtree_control, which is root-only on a real cgroup-v2 root —
// the gate makes the failure mode "must run as root" instead of an EACCES
// inside cgroupcheck.Probe. The read-only --no-probe path stays
// unrestricted, covered by the sibling --no-probe test below.
func TestCgroupsCmdProbeRequiresRoot(t *testing.T) {
	restore := kukshared.SetGeteuidForTesting(func() int { return 1000 })
	t.Cleanup(restore)

	root := writeFakeCgroup(t,
		"cpuset cpu io memory pids",
		"cpu pids",
	)

	_, _, err := runCmd(t, "--root", root, "--probe")
	if err == nil {
		t.Fatal("Execute() error = nil under euid=1000 with --probe, want ErrMustRunAsRoot")
	}
	if !errors.Is(err, errdefs.ErrMustRunAsRoot) {
		t.Fatalf("error does not wrap ErrMustRunAsRoot: %v", err)
	}
	if !strings.Contains(err.Error(), "kuke doctor cgroups --probe") {
		t.Errorf("error does not name the subcommand: %v", err)
	}
}

// TestCgroupsCmdNoProbeBypassesRootGate confirms the read-only --no-probe
// path stays available to non-root operators — it only reads cgroup files
// and never writes, so the UID gate must not fire.
func TestCgroupsCmdNoProbeBypassesRootGate(t *testing.T) {
	restore := kukshared.SetGeteuidForTesting(func() int { return 1000 })
	t.Cleanup(restore)

	root := writeFakeCgroup(t,
		"cpuset cpu io memory hugetlb pids rdma misc",
		"cpu memory io pids",
	)

	if _, _, err := runCmd(t, "--root", root, "--no-probe"); err != nil {
		t.Fatalf("--no-probe rejected under euid=1000: %v", err)
	}
}

// TestCgroupsCmdNameWithoutScope: a positional name without --scope is a
// usage mistake — surface it cleanly instead of silently ignoring the
// argument.
func TestCgroupsCmdNameWithoutScope(t *testing.T) {
	root := writeFakeCgroup(t,
		"cpuset cpu io memory hugetlb pids rdma misc",
		"cpu memory io pids",
	)
	_, _, err := runCmd(t, "--root", root, "default")
	if err == nil {
		t.Fatal("Execute() error = nil for name without --scope, want error")
	}
	if !strings.Contains(err.Error(), "--scope") {
		t.Errorf("error = %q, want it to mention --scope", err.Error())
	}
}
