// 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 attach_test

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"

	attachcmd "github.com/eminwux/kukeon/cmd/kuke/attach"
	"github.com/eminwux/kukeon/internal/errdefs"
	"github.com/eminwux/kukeon/pkg/api/kukeonv1"
	v1beta1 "github.com/eminwux/kukeon/pkg/api/model/v1beta1"
	sbshattach "github.com/eminwux/sbsh/pkg/attach"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

const (
	testHostSocket = "/opt/kukeon/r1/s1/st1/c1/work/tty/socket"
)

type fakeClient struct {
	kukeonv1.FakeClient

	listContainersFn  func(realm, space, stack, cell string) ([]v1beta1.ContainerSpec, error)
	getCellFn         func(doc v1beta1.CellDoc) (kukeonv1.GetCellResult, error)
	attachContainerFn func(doc v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error)
}

// GetCell backs the divergence guard. With no getCellFn set it reports a
// healthy Ready cell whose root task is live, so attach proceeds — the default
// for every test not exercising the divergence path.
func (f *fakeClient) GetCell(
	_ context.Context,
	doc v1beta1.CellDoc,
) (kukeonv1.GetCellResult, error) {
	if f.getCellFn != nil {
		return f.getCellFn(doc)
	}
	return kukeonv1.GetCellResult{
		Cell:                     v1beta1.CellDoc{Status: v1beta1.CellStatus{State: v1beta1.CellStateReady}},
		MetadataExists:           true,
		RootContainerExists:      true,
		RootContainerTaskRunning: true,
	}, nil
}

func (f *fakeClient) ListContainers(
	_ context.Context,
	realm, space, stack, cell string,
) ([]v1beta1.ContainerSpec, error) {
	if f.listContainersFn == nil {
		return nil, errors.New("unexpected ListContainers call")
	}
	return f.listContainersFn(realm, space, stack, cell)
}

func (f *fakeClient) AttachContainer(
	_ context.Context,
	doc v1beta1.ContainerDoc,
) (kukeonv1.AttachContainerResult, error) {
	if f.attachContainerFn == nil {
		return kukeonv1.AttachContainerResult{}, errors.New("unexpected AttachContainer call")
	}
	return f.attachContainerFn(doc)
}

// runCapture records the Options passed to pkg/attach.Run and returns
// nil so the test treats the call as a clean detach.
type runCapture struct {
	calls int
	opts  sbshattach.Options
}

func (r *runCapture) fn(_ context.Context, opts sbshattach.Options) error {
	r.calls++
	r.opts = opts
	return nil
}

// runReturning is the mirror of runCapture for tests that want pkg/attach.Run
// to return a specific error (rather than the clean-detach default).
type runReturning struct {
	calls int
	err   error
}

func (r *runReturning) fn(_ context.Context, _ sbshattach.Options) error {
	r.calls++
	return r.err
}

func newCmdWithCtx(t *testing.T, fc *fakeClient, run *runCapture) *cobra.Command {
	t.Helper()

	cmd := attachcmd.NewAttachCmd()
	ctx := context.Background()
	if fc != nil {
		ctx = context.WithValue(ctx, attachcmd.MockControllerKey{}, kukeonv1.Client(fc))
	}
	if run != nil {
		ctx = context.WithValue(ctx, attachcmd.MockRunKey{}, attachcmd.RunFn(run.fn))
	}
	cmd.SetContext(ctx)
	cmd.SilenceErrors = true
	cmd.SilenceUsage = true
	return cmd
}

func TestAttach_SingleNonRootAttachable_Succeeds(t *testing.T) {
	t.Cleanup(viper.Reset)

	fc := &fakeClient{
		listContainersFn: func(_, _, _, _ string) ([]v1beta1.ContainerSpec, error) {
			return []v1beta1.ContainerSpec{
				{ID: "root", Root: true, Attachable: false},
				{ID: "work", Attachable: true},
			}, nil
		},
		attachContainerFn: func(doc v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			if got := doc.Metadata.Name; got != "work" {
				t.Errorf("AttachContainer called with name %q, want %q", got, "work")
			}
			return kukeonv1.AttachContainerResult{HostSocketPath: testHostSocket}, nil
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1", "c1",
	})

	if err := cmd.Execute(); err != nil {
		t.Fatalf("Execute returned error: %v", err)
	}
	if run.calls != 1 {
		t.Fatalf("attach.Run called %d times, want 1", run.calls)
	}
	if run.opts.SocketPath != testHostSocket {
		t.Errorf("Options.SocketPath = %q, want %q", run.opts.SocketPath, testHostSocket)
	}
}

// TestAttach_PositionalCell_DefaultsRealmSpaceStack covers the issue-#373
// contract: `kuke attach <cell>` (no realm/space/stack flags) targets the
// user-default hierarchy `default/default/default`. The runtime resolves
// realm/space/stack from the flag defaults wired into NewAttachCmd.
func TestAttach_PositionalCell_DefaultsRealmSpaceStack(t *testing.T) {
	t.Cleanup(viper.Reset)

	var gotRealm, gotSpace, gotStack, gotCell string
	fc := &fakeClient{
		listContainersFn: func(realm, space, stack, cell string) ([]v1beta1.ContainerSpec, error) {
			gotRealm, gotSpace, gotStack, gotCell = realm, space, stack, cell
			return []v1beta1.ContainerSpec{
				{ID: "root", Root: true},
				{ID: "work", Attachable: true},
			}, nil
		},
		attachContainerFn: func(doc v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			if got := doc.Spec.RealmID; got != "default" {
				t.Errorf("doc.Spec.RealmID = %q, want %q", got, "default")
			}
			if got := doc.Spec.SpaceID; got != "default" {
				t.Errorf("doc.Spec.SpaceID = %q, want %q", got, "default")
			}
			if got := doc.Spec.StackID; got != "default" {
				t.Errorf("doc.Spec.StackID = %q, want %q", got, "default")
			}
			if got := doc.Spec.CellID; got != "hello" {
				t.Errorf("doc.Spec.CellID = %q, want %q", got, "hello")
			}
			return kukeonv1.AttachContainerResult{HostSocketPath: testHostSocket}, nil
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{"hello"})

	if err := cmd.Execute(); err != nil {
		t.Fatalf("Execute returned error: %v", err)
	}
	if gotRealm != "default" || gotSpace != "default" || gotStack != "default" || gotCell != "hello" {
		t.Errorf("ListContainers called with (%q, %q, %q, %q), want (default, default, default, hello)",
			gotRealm, gotSpace, gotStack, gotCell)
	}
	if run.calls != 1 {
		t.Fatalf("attach.Run called %d times, want 1", run.calls)
	}
}

// TestAttach_RejectsCellFlag locks in the breaking change: the legacy
// `--cell` flag was removed in favor of the positional arg. The cobra
// parser must reject `--cell` rather than silently accepting it.
func TestAttach_RejectsCellFlag(t *testing.T) {
	t.Cleanup(viper.Reset)

	cmd := newCmdWithCtx(t, &fakeClient{}, &runCapture{})
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1", "--cell", "c1",
	})

	err := cmd.Execute()
	if err == nil {
		t.Fatalf("Execute returned nil, want unknown-flag error")
	}
	if !strings.Contains(err.Error(), "unknown flag") {
		t.Errorf("error %q does not mention unknown flag", err.Error())
	}
}

func TestAttach_AmbiguousCandidates_ErrorsWithList(t *testing.T) {
	t.Cleanup(viper.Reset)

	fc := &fakeClient{
		listContainersFn: func(_, _, _, _ string) ([]v1beta1.ContainerSpec, error) {
			return []v1beta1.ContainerSpec{
				{ID: "root", Root: true},
				{ID: "shell", Attachable: true},
				{ID: "claude", Attachable: true},
			}, nil
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1", "c1",
	})

	err := cmd.Execute()
	if err == nil {
		t.Fatalf("Execute returned nil, want ErrAttachAmbiguous")
	}
	if !errors.Is(err, errdefs.ErrAttachAmbiguous) {
		t.Fatalf("error %v does not unwrap to ErrAttachAmbiguous", err)
	}
	// The candidate list must be sorted and present in the error message so
	// the operator can re-run with --container.
	if got := err.Error(); !strings.Contains(got, "claude, shell") {
		t.Errorf("error message %q missing sorted candidate list", got)
	}
	if run.calls != 0 {
		t.Errorf("attach.Run called %d times on ambiguous picker, want 0", run.calls)
	}
}

func TestAttach_NoCandidate_Errors(t *testing.T) {
	t.Cleanup(viper.Reset)

	fc := &fakeClient{
		listContainersFn: func(_, _, _, _ string) ([]v1beta1.ContainerSpec, error) {
			return []v1beta1.ContainerSpec{
				{ID: "root", Root: true},
				// non-attachable workload doesn't count
				{ID: "side", Attachable: false},
			}, nil
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1", "c1",
	})

	err := cmd.Execute()
	if !errors.Is(err, errdefs.ErrAttachNoCandidate) {
		t.Fatalf("error %v does not unwrap to ErrAttachNoCandidate", err)
	}
	if run.calls != 0 {
		t.Errorf("attach.Run called %d times on empty picker, want 0", run.calls)
	}
}

func TestAttach_RootContainerExcludedFromAutoPick(t *testing.T) {
	t.Cleanup(viper.Reset)

	// A root container with Attachable=true must NOT be picked: the issue
	// excludes the root container from the auto-pick set explicitly.
	fc := &fakeClient{
		listContainersFn: func(_, _, _, _ string) ([]v1beta1.ContainerSpec, error) {
			return []v1beta1.ContainerSpec{
				{ID: "root", Root: true, Attachable: true},
			}, nil
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1", "c1",
	})

	err := cmd.Execute()
	if !errors.Is(err, errdefs.ErrAttachNoCandidate) {
		t.Fatalf("error %v does not unwrap to ErrAttachNoCandidate (root must be excluded from auto-pick)", err)
	}
}

func TestAttach_ExplicitContainer_NotAttachable_SurfacesSentinel(t *testing.T) {
	t.Cleanup(viper.Reset)

	fc := &fakeClient{
		// ListContainers must not be called when --container is explicit.
		attachContainerFn: func(doc v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			if got := doc.Metadata.Name; got != "side" {
				t.Errorf("AttachContainer called with name %q, want %q", got, "side")
			}
			return kukeonv1.AttachContainerResult{}, errdefs.ErrAttachNotSupported
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1",
		"--container", "side", "c1",
	})

	err := cmd.Execute()
	if !errors.Is(err, errdefs.ErrAttachNotSupported) {
		t.Fatalf("error %v does not unwrap to ErrAttachNotSupported", err)
	}
	if run.calls != 0 {
		t.Errorf("attach.Run called %d times on non-attachable target, want 0", run.calls)
	}
}

// TestAttach_ExplicitContainer_NotFound_SurfacesSentinel locks in the
// ErrContainerNotFound branch's %w wrap: when AttachContainer's RPC reports
// the named container doesn't exist, kuke attach must propagate the
// sentinel so upstream callers can still errors.Is it.
func TestAttach_ExplicitContainer_NotFound_SurfacesSentinel(t *testing.T) {
	t.Cleanup(viper.Reset)

	fc := &fakeClient{
		attachContainerFn: func(_ v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			return kukeonv1.AttachContainerResult{}, errdefs.ErrContainerNotFound
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1",
		"--container", "ghost", "c1",
	})

	err := cmd.Execute()
	if !errors.Is(err, errdefs.ErrContainerNotFound) {
		t.Fatalf("error %v does not unwrap to ErrContainerNotFound", err)
	}
	if run.calls != 0 {
		t.Errorf("attach.Run called %d times on missing target, want 0", run.calls)
	}
}

func TestAttach_ExplicitContainer_Attachable_Succeeds(t *testing.T) {
	t.Cleanup(viper.Reset)

	fc := &fakeClient{
		attachContainerFn: func(_ v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			return kukeonv1.AttachContainerResult{HostSocketPath: testHostSocket}, nil
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1",
		"--container", "shell", "c1",
	})

	if err := cmd.Execute(); err != nil {
		t.Fatalf("Execute returned error: %v", err)
	}
	if run.calls != 1 {
		t.Fatalf("attach.Run called %d times, want 1", run.calls)
	}
	if run.opts.SocketPath != testHostSocket {
		t.Errorf("Options.SocketPath = %q, want %q", run.opts.SocketPath, testHostSocket)
	}
}

// TestAttach_RecordedReady_TaskGone_Refuses covers #683: the cell metadata
// records Ready and the containerd container record still exists (it survives a
// host/daemon restart), but the backing root task is gone. Attach must refuse
// with the diverged-state error and never reach AttachContainer / the sbsh
// loop — otherwise it hands back a socket backed by a dead task.
func TestAttach_RecordedReady_TaskGone_Refuses(t *testing.T) {
	t.Cleanup(viper.Reset)

	attachCalls := 0
	fc := &fakeClient{
		getCellFn: func(_ v1beta1.CellDoc) (kukeonv1.GetCellResult, error) {
			return kukeonv1.GetCellResult{
				Cell:           v1beta1.CellDoc{Status: v1beta1.CellStatus{State: v1beta1.CellStateReady}},
				MetadataExists: true,
				// Record survived the restart; the task did not.
				RootContainerExists:      true,
				RootContainerTaskRunning: false,
			}, nil
		},
		attachContainerFn: func(_ v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			attachCalls++
			return kukeonv1.AttachContainerResult{HostSocketPath: testHostSocket}, nil
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1",
		"--container", "work", "c1",
	})

	err := cmd.Execute()
	if err == nil {
		t.Fatal("Execute returned nil, want divergence refusal")
	}
	if !strings.Contains(err.Error(), "diverged") {
		t.Errorf("error %q missing divergence wording", err.Error())
	}
	if !strings.Contains(err.Error(), "kuke delete cell c1") {
		t.Errorf("error %q missing `kuke delete cell` recovery pointer", err.Error())
	}
	if attachCalls != 0 {
		t.Errorf("AttachContainer calls=%d, want 0 (must not attach to a dead socket)", attachCalls)
	}
	if run.calls != 0 {
		t.Errorf("attach.Run called %d times, want 0", run.calls)
	}
}

// TestAttach_RunReturnsCleanDetachError covers the regression from
// #147: when the user presses Ctrl+] Ctrl+], sbsh's controller fires
// the Detach RPC and pkg/attach.Run returns attach.ErrDetached
// (sbsh#192, available since sbsh v0.10.1). The wrapper must recognise
// the public sentinel as a benign session end and return nil so `kuke
// attach` exits 0.
func TestAttach_RunReturnsCleanDetachError(t *testing.T) {
	t.Cleanup(viper.Reset)

	fc := &fakeClient{
		attachContainerFn: func(_ v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			return kukeonv1.AttachContainerResult{HostSocketPath: testHostSocket}, nil
		},
	}
	cleanDetachErr := fmt.Errorf("wrapped by harness: %w", sbshattach.ErrDetached)
	run := &runReturning{err: cleanDetachErr}
	cmd := newCmdWithCtx(t, fc, nil)
	cmd.SetContext(context.WithValue(cmd.Context(), attachcmd.MockRunKey{}, attachcmd.RunFn(run.fn)))
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1",
		"--container", "work", "c1",
	})

	if err := cmd.Execute(); err != nil {
		t.Fatalf("Execute returned %v on clean-detach error, want nil", err)
	}
	if run.calls != 1 {
		t.Fatalf("attach.Run called %d times, want 1", run.calls)
	}
}

// TestAttach_RunReturnsPeerClosedError mirrors the clean-detach case for
// the other benign session-end signal: pkg/attach.Run returns
// attach.ErrPeerClosed when the remote terminal drops the IO connection
// (workload exited, peer crashed). For `kuke attach` (a peer attach,
// not the cell owner), this is also exit 0 — same semantics as detach.
func TestAttach_RunReturnsPeerClosedError(t *testing.T) {
	t.Cleanup(viper.Reset)

	fc := &fakeClient{
		attachContainerFn: func(_ v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			return kukeonv1.AttachContainerResult{HostSocketPath: testHostSocket}, nil
		},
	}
	peerClosedErr := fmt.Errorf("wrapped by harness: %w", sbshattach.ErrPeerClosed)
	run := &runReturning{err: peerClosedErr}
	cmd := newCmdWithCtx(t, fc, nil)
	cmd.SetContext(context.WithValue(cmd.Context(), attachcmd.MockRunKey{}, attachcmd.RunFn(run.fn)))
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1",
		"--container", "work", "c1",
	})

	if err := cmd.Execute(); err != nil {
		t.Fatalf("Execute returned %v on peer-closed error, want nil", err)
	}
	if run.calls != 1 {
		t.Fatalf("attach.Run called %d times, want 1", run.calls)
	}
}

// TestAttach_RunReturnsRealError ensures that any pkg/attach.Run error
// not matching the clean-detach signature still bubbles up — the wrapper
// must not swallow real failures like socket-not-found or RPC errors.
func TestAttach_RunReturnsRealError(t *testing.T) {
	t.Cleanup(viper.Reset)

	fc := &fakeClient{
		attachContainerFn: func(_ v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			return kukeonv1.AttachContainerResult{HostSocketPath: testHostSocket}, nil
		},
	}
	bootErr := errors.New("wait on ready: dial unix: connection refused")
	run := &runReturning{err: bootErr}
	cmd := newCmdWithCtx(t, fc, nil)
	cmd.SetContext(context.WithValue(cmd.Context(), attachcmd.MockRunKey{}, attachcmd.RunFn(run.fn)))
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1",
		"--container", "work", "c1",
	})

	err := cmd.Execute()
	if err == nil {
		t.Fatalf("Execute returned nil on real error, want %v", bootErr)
	}
	if !errors.Is(err, bootErr) {
		t.Fatalf("error %v does not unwrap to bootErr", err)
	}
}

func TestAttach_MissingFlags(t *testing.T) {
	t.Cleanup(viper.Reset)

	cases := []struct {
		name    string
		args    []string
		wantErr error
	}{
		{
			name:    "explicit empty realm",
			args:    []string{"--realm", "", "--space", "s1", "--stack", "st1", "c1"},
			wantErr: errdefs.ErrRealmNameRequired,
		},
		{
			name:    "explicit empty space",
			args:    []string{"--realm", "r1", "--space", "", "--stack", "st1", "c1"},
			wantErr: errdefs.ErrSpaceNameRequired,
		},
		{
			name:    "explicit empty stack",
			args:    []string{"--realm", "r1", "--space", "s1", "--stack", "", "c1"},
			wantErr: errdefs.ErrStackNameRequired,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			// Reset clears viper's defaults set by DefineKV at package
			// init so an explicit `--realm ""` resolves to the empty
			// string and trips the runtime guard. t.Setenv keeps the
			// env-var path quiet too, in case the dev box exports a
			// KUKE_ATTACH_*.
			viper.Reset()
			t.Setenv("KUKE_ATTACH_REALM", "")
			t.Setenv("KUKE_ATTACH_SPACE", "")
			t.Setenv("KUKE_ATTACH_STACK", "")

			cmd := newCmdWithCtx(t, &fakeClient{}, &runCapture{})
			cmd.SetArgs(tc.args)

			err := cmd.Execute()
			if !errors.Is(err, tc.wantErr) {
				t.Fatalf("error %v does not unwrap to %v", err, tc.wantErr)
			}
		})
	}
}

// TestAttach_MissingCellArg covers the cobra arg-count guard: omitting the
// positional <cell> exits before runAttach ever runs, surfacing a
// usage-style error rather than ErrCellNameRequired.
func TestAttach_MissingCellArg(t *testing.T) {
	t.Cleanup(viper.Reset)

	cmd := newCmdWithCtx(t, &fakeClient{}, &runCapture{})
	cmd.SetArgs([]string{"--realm", "r1", "--space", "s1", "--stack", "st1"})

	err := cmd.Execute()
	if err == nil {
		t.Fatalf("Execute returned nil, want arg-count error")
	}
	if !strings.Contains(err.Error(), "accepts 1 arg") {
		t.Errorf("error %q does not mention arg-count requirement", err.Error())
	}
}

// TestAttach_NotCreated_PointsAtRun covers #852: the cell-level liveness
// guard fires before ListContainers, so a cell that has never been
// created surfaces the operator-actionable "create it first" pointer
// instead of falling out of the container picker with the unhelpful
// ErrAttachNoCandidate. Also verifies the guard short-circuits before
// reaching AttachContainer.
func TestAttach_NotCreated_PointsAtRun(t *testing.T) {
	t.Cleanup(viper.Reset)

	listCalls := 0
	attachCalls := 0
	fc := &fakeClient{
		getCellFn: func(_ v1beta1.CellDoc) (kukeonv1.GetCellResult, error) {
			return kukeonv1.GetCellResult{MetadataExists: false}, nil
		},
		listContainersFn: func(_, _, _, _ string) ([]v1beta1.ContainerSpec, error) {
			listCalls++
			return nil, nil
		},
		attachContainerFn: func(_ v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			attachCalls++
			return kukeonv1.AttachContainerResult{}, nil
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1", "kukeon-pm-0",
	})

	err := cmd.Execute()
	if err == nil {
		t.Fatal("Execute returned nil on missing cell, want NotCreated error")
	}
	if got := err.Error(); !strings.Contains(got, "does not exist") {
		t.Errorf("error %q missing %q wording", got, "does not exist")
	}
	if got := err.Error(); !strings.Contains(got, "kuke run") {
		t.Errorf("error %q missing `kuke run` recovery pointer", got)
	}
	if listCalls != 0 {
		t.Errorf("ListContainers called %d times, want 0 (guard must fire first)", listCalls)
	}
	if attachCalls != 0 {
		t.Errorf("AttachContainer called %d times, want 0", attachCalls)
	}
	if run.calls != 0 {
		t.Errorf("attach.Run called %d times, want 0", run.calls)
	}
}

// TestAttach_Stopped_PointsAtStart covers #852's repro signature: the
// wind-down flow leaves the cell at Stopped with an orphan socket
// inode, and the previous guard short-circuited on any non-Ready state.
// Attach must now refuse with the start-it-first pointer instead of
// dialing through to `connection refused`.
func TestAttach_Stopped_PointsAtStart(t *testing.T) {
	t.Cleanup(viper.Reset)

	attachCalls := 0
	fc := &fakeClient{
		getCellFn: func(_ v1beta1.CellDoc) (kukeonv1.GetCellResult, error) {
			return kukeonv1.GetCellResult{
				Cell:                v1beta1.CellDoc{Status: v1beta1.CellStatus{State: v1beta1.CellStateStopped}},
				MetadataExists:      true,
				RootContainerExists: true,
				// Wind-down may leave the root task still alive (long-lived
				// `sleep infinity`); the workload task is gone and that is
				// what makes the cell unattachable.
				RootContainerTaskRunning: true,
			}, nil
		},
		attachContainerFn: func(_ v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			attachCalls++
			return kukeonv1.AttachContainerResult{HostSocketPath: testHostSocket}, nil
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1",
		"--container", "work", "kukeon-pm-0",
	})

	err := cmd.Execute()
	if err == nil {
		t.Fatal("Execute returned nil on Stopped cell, want start-it-first error")
	}
	if got := err.Error(); !strings.Contains(got, "Stopped") {
		t.Errorf("error %q missing %q state name", got, "Stopped")
	}
	if got := err.Error(); !strings.Contains(got, "kuke start kukeon-pm-0") {
		t.Errorf("error %q missing `kuke start kukeon-pm-0` recovery pointer", got)
	}
	if attachCalls != 0 {
		t.Errorf("AttachContainer called %d times, want 0 (must not dial dead socket)", attachCalls)
	}
	if run.calls != 0 {
		t.Errorf("attach.Run called %d times, want 0", run.calls)
	}
}

// TestAttach_Failed_PointsAtDelete covers the Failed branch of the
// generalized guard. A Failed cell is sticky per the reconciler, so the
// only recovery is delete-then-rerun — same pointer as the
// Ready+task-dead divergence path.
func TestAttach_Failed_PointsAtDelete(t *testing.T) {
	t.Cleanup(viper.Reset)

	attachCalls := 0
	fc := &fakeClient{
		getCellFn: func(_ v1beta1.CellDoc) (kukeonv1.GetCellResult, error) {
			return kukeonv1.GetCellResult{
				Cell:                v1beta1.CellDoc{Status: v1beta1.CellStatus{State: v1beta1.CellStateFailed}},
				MetadataExists:      true,
				RootContainerExists: true,
			}, nil
		},
		attachContainerFn: func(_ v1beta1.ContainerDoc) (kukeonv1.AttachContainerResult, error) {
			attachCalls++
			return kukeonv1.AttachContainerResult{HostSocketPath: testHostSocket}, nil
		},
	}
	run := &runCapture{}
	cmd := newCmdWithCtx(t, fc, run)
	cmd.SetArgs([]string{
		"--realm", "r1", "--space", "s1", "--stack", "st1",
		"--container", "work", "c1",
	})

	err := cmd.Execute()
	if err == nil {
		t.Fatal("Execute returned nil on Failed cell, want delete-it-first error")
	}
	if got := err.Error(); !strings.Contains(got, "Failed") {
		t.Errorf("error %q missing %q state name", got, "Failed")
	}
	if got := err.Error(); !strings.Contains(got, "kuke delete cell c1") {
		t.Errorf("error %q missing `kuke delete cell c1` recovery pointer", got)
	}
	if attachCalls != 0 {
		t.Errorf("AttachContainer called %d times, want 0", attachCalls)
	}
	if run.calls != 0 {
		t.Errorf("attach.Run called %d times, want 0", run.calls)
	}
}
