package launch

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/ollama/ollama/cmd/internal/fileutil"
	"github.com/ollama/ollama/types/model"
)

func TestOpenclawIntegration(t *testing.T) {
	c := &Openclaw{}

	t.Run("String", func(t *testing.T) {
		if got := c.String(); got != "OpenClaw" {
			t.Errorf("String() = %q, want %q", got, "OpenClaw")
		}
	})

	t.Run("implements Runner", func(t *testing.T) {
		var _ Runner = c
	})

	t.Run("implements Editor", func(t *testing.T) {
		var _ Editor = c
	})
}

func TestOpenclawRunPassthroughArgs(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("uses a POSIX shell test binary")
	}

	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	t.Setenv("PATH", tmpDir)

	if err := integrationOnboarded("openclaw"); err != nil {
		t.Fatal(err)
	}

	configDir := filepath.Join(tmpDir, ".openclaw")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{
		"wizard": {"lastRunAt": "2026-01-01T00:00:00Z"}
	}`), 0o644); err != nil {
		t.Fatal(err)
	}

	bin := filepath.Join(tmpDir, "openclaw")
	if err := os.WriteFile(bin, []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$HOME/invocations.log\"\n"), 0o755); err != nil {
		t.Fatal(err)
	}

	oldInteractive := isInteractiveSession
	isInteractiveSession = func() bool { return true }
	defer func() { isInteractiveSession = oldInteractive }()

	oldConfirmPrompt := DefaultConfirmPrompt
	DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
		t.Fatalf("did not expect confirmation prompt during passthrough launch: %s", prompt)
		return false, nil
	}
	defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

	c := &Openclaw{}
	if err := c.Run("llama3.2", nil, []string{"gateway", "--someflag"}); err != nil {
		t.Fatalf("Run() error = %v", err)
	}

	data, err := os.ReadFile(filepath.Join(tmpDir, "invocations.log"))
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	if len(lines) != 1 {
		t.Fatalf("expected exactly 1 invocation, got %d: %v", len(lines), lines)
	}
	if lines[0] != "gateway --someflag" {
		t.Fatalf("invocation = %q, want %q", lines[0], "gateway --someflag")
	}
}

func TestOpenclawRun_ChannelSetupHappensBeforeGatewayRestart(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("uses a POSIX shell test binary")
	}

	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	t.Setenv("PATH", tmpDir)

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	defer ln.Close()
	port := ln.Addr().(*net.TCPAddr).Port

	configDir := filepath.Join(tmpDir, ".openclaw")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(fmt.Sprintf(`{
		"wizard": {"lastRunAt": "2026-01-01T00:00:00Z"},
		"gateway": {"port": %d}
	}`, port)), 0o644); err != nil {
		t.Fatal(err)
	}

	bin := filepath.Join(tmpDir, "openclaw")
	script := fmt.Sprintf(`#!/bin/sh
printf '%%s\n' "$*" >> "$HOME/invocations.log"
if [ "$1" = "channels" ] && [ "$2" = "add" ]; then
  /bin/mkdir -p "$HOME/.openclaw"
  /bin/cat > "$HOME/.openclaw/openclaw.json" <<'EOF'
{"wizard":{"lastRunAt":"2026-01-01T00:00:00Z"},"gateway":{"port":%d},"channels":{"telegram":{"botToken":"configured"}}}
EOF
fi
`, port)
	if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
		t.Fatal(err)
	}

	oldInteractive := isInteractiveSession
	isInteractiveSession = func() bool { return true }
	defer func() { isInteractiveSession = oldInteractive }()

	promptCount := 0
	oldConfirmPrompt := DefaultConfirmPrompt
	DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
		promptCount++
		if prompt != "Connect a channel (messaging app) now?" {
			t.Fatalf("unexpected prompt: %q", prompt)
		}
		return true, nil
	}
	defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

	c := &Openclaw{}
	if err := c.Run("llama3.2", nil, nil); err != nil {
		t.Fatalf("Run() error = %v", err)
	}

	if promptCount != 1 {
		t.Fatalf("expected one channel setup prompt, got %d", promptCount)
	}

	data, err := os.ReadFile(filepath.Join(tmpDir, "invocations.log"))
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	if len(lines) < 3 {
		t.Fatalf("expected at least 3 invocations (channels add, daemon restart, tui), got %v", lines)
	}
	if lines[0] != "channels add" {
		t.Fatalf("expected first invocation to be channels setup, got %q", lines[0])
	}
	if lines[1] != "daemon restart" {
		t.Fatalf("expected second invocation to be daemon restart, got %q", lines[1])
	}
	if lines[2] != "tui" {
		t.Fatalf("expected third invocation to be tui, got %q", lines[2])
	}
}

func TestOpenclawRun_SetupLaterContinuesToGatewayAndTUI(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("uses a POSIX shell test binary")
	}

	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	t.Setenv("PATH", tmpDir)

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	defer ln.Close()
	port := ln.Addr().(*net.TCPAddr).Port

	configDir := filepath.Join(tmpDir, ".openclaw")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(fmt.Sprintf(`{
		"wizard": {"lastRunAt": "2026-01-01T00:00:00Z"},
		"gateway": {"port": %d}
	}`, port)), 0o644); err != nil {
		t.Fatal(err)
	}

	bin := filepath.Join(tmpDir, "openclaw")
	if err := os.WriteFile(bin, []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$HOME/invocations.log\"\n"), 0o755); err != nil {
		t.Fatal(err)
	}

	oldInteractive := isInteractiveSession
	isInteractiveSession = func() bool { return true }
	defer func() { isInteractiveSession = oldInteractive }()

	promptCount := 0
	oldConfirmPrompt := DefaultConfirmPrompt
	DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
		promptCount++
		return false, nil
	}
	defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

	c := &Openclaw{}
	if err := c.Run("llama3.2", nil, nil); err != nil {
		t.Fatalf("Run() error = %v", err)
	}

	if promptCount != 1 {
		t.Fatalf("expected one channel setup prompt, got %d", promptCount)
	}
	data, err := os.ReadFile(filepath.Join(tmpDir, "invocations.log"))
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	if len(lines) < 2 {
		t.Fatalf("expected at least 2 invocations (daemon restart, tui), got %v", lines)
	}
	if lines[0] != "daemon restart" {
		t.Fatalf("expected first invocation to be daemon restart, got %q", lines[0])
	}
	if lines[1] != "tui" {
		t.Fatalf("expected second invocation to be tui, got %q", lines[1])
	}
	for _, line := range lines {
		if line == "channels add" {
			t.Fatalf("did not expect channels add invocation after choosing set up later, got %v", lines)
		}
	}
}

func TestOpenclawRun_FirstLaunchOnboardUsesLaunchManagedHealthFlow(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("uses a POSIX shell test binary")
	}

	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	t.Setenv("PATH", tmpDir)

	bin := filepath.Join(tmpDir, "openclaw")
	script := fmt.Sprintf(`#!/bin/sh
printf '%%s\n' "$*" >> "$HOME/invocations.log"
if [ "$1" = "onboard" ]; then
  /usr/bin/env | /usr/bin/sort > "$HOME/onboard-env.log"
  /bin/mkdir -p "$HOME/.openclaw"
  /bin/cat > "$HOME/.openclaw/openclaw.json" <<'EOF'
{"wizard":{"lastRunAt":"2026-01-01T00:00:00Z"},"gateway":{"port":18789,"mode":"local"}}
EOF
fi
exit 0
`)
	if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
		t.Fatal(err)
	}

	oldConfirmPrompt := DefaultConfirmPrompt
	DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
		if prompt != "I understand the risks. Continue?" {
			t.Fatalf("unexpected prompt: %q", prompt)
		}
		return true, nil
	}
	defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

	c := &Openclaw{}
	if err := c.Run("llama3.2", nil, []string{"status"}); err != nil {
		t.Fatalf("Run() error = %v", err)
	}

	data, err := os.ReadFile(filepath.Join(tmpDir, "invocations.log"))
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	if len(lines) < 2 {
		t.Fatalf("expected onboard + passthrough invocations, got %v", lines)
	}
	onboardInvocation := ""
	for _, line := range lines {
		if strings.HasPrefix(line, "onboard ") {
			onboardInvocation = line
			break
		}
	}
	if onboardInvocation == "" {
		t.Fatalf("expected onboard invocation, got %v", lines)
	}
	if !strings.Contains(onboardInvocation, "--skip-health") {
		t.Fatalf("expected onboard invocation to include --skip-health, got %q", onboardInvocation)
	}

	envData, err := os.ReadFile(filepath.Join(tmpDir, "onboard-env.log"))
	if err != nil {
		t.Fatal(err)
	}
	env := envSliceToMap(strings.Split(strings.TrimSpace(string(envData)), "\n"))
	if env["OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS"] != "1" {
		t.Fatalf("OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS = %q, want %q", env["OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS"], "1")
	}
	if env["OPENCLAW_PLUGIN_STAGE_DIR"] != filepath.Join(tmpDir, ".openclaw", "plugin-runtime-deps") {
		t.Fatalf("OPENCLAW_PLUGIN_STAGE_DIR = %q, want %q", env["OPENCLAW_PLUGIN_STAGE_DIR"], filepath.Join(tmpDir, ".openclaw", "plugin-runtime-deps"))
	}
}

func TestOpenclawRun_FirstLaunchTUIArgsEnsureGatewayBeforePassthrough(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("uses a POSIX shell test binary")
	}

	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	t.Setenv("PATH", tmpDir)

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	defer ln.Close()
	port := ln.Addr().(*net.TCPAddr).Port

	bin := filepath.Join(tmpDir, "openclaw")
	script := fmt.Sprintf(`#!/bin/sh
printf '%%s\n' "$*" >> "$HOME/invocations.log"
if [ "$1" = "onboard" ]; then
  /bin/mkdir -p "$HOME/.openclaw"
  /bin/cat > "$HOME/.openclaw/openclaw.json" <<'EOF'
{"wizard":{"lastRunAt":"2026-01-01T00:00:00Z"},"gateway":{"port":%d,"mode":"local"}}
EOF
fi
exit 0
`, port)
	if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
		t.Fatal(err)
	}

	oldConfirmPrompt := DefaultConfirmPrompt
	DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
		if prompt != "I understand the risks. Continue?" {
			t.Fatalf("unexpected prompt: %q", prompt)
		}
		return true, nil
	}
	defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

	c := &Openclaw{}
	if err := c.Run("llama3.2", nil, []string{"tui"}); err != nil {
		t.Fatalf("Run() error = %v", err)
	}

	data, err := os.ReadFile(filepath.Join(tmpDir, "invocations.log"))
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	if len(lines) < 3 {
		t.Fatalf("expected at least 3 invocations (update, onboard, daemon restart, tui), got %v", lines)
	}
	onboardIdx, daemonRestartIdx, tuiIdx := -1, -1, -1
	for i, line := range lines {
		if onboardIdx == -1 && strings.HasPrefix(line, "onboard ") {
			onboardIdx = i
		}
		if daemonRestartIdx == -1 && line == "daemon restart" {
			daemonRestartIdx = i
		}
		if tuiIdx == -1 && line == "tui" {
			tuiIdx = i
		}
	}
	if onboardIdx == -1 {
		t.Fatalf("expected an onboarding invocation, got %v", lines)
	}
	if daemonRestartIdx == -1 {
		t.Fatalf("expected a daemon restart before tui, got %v", lines)
	}
	if tuiIdx == -1 {
		t.Fatalf("expected a tui invocation, got %v", lines)
	}
	if !(onboardIdx < daemonRestartIdx && daemonRestartIdx < tuiIdx) {
		t.Fatalf("expected onboarding, then daemon restart, then tui; got %v", lines)
	}
}

func TestOpenclawEnsureGatewayReady_UsesDaemonStartFallback(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("uses a POSIX shell test binary")
	}

	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	t.Setenv("PATH", tmpDir)

	portProbe, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	port := portProbe.Addr().(*net.TCPAddr).Port
	_ = portProbe.Close()

	configDir := filepath.Join(tmpDir, ".openclaw")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(fmt.Sprintf(`{
		"wizard": {"lastRunAt": "2026-01-01T00:00:00Z"},
		"gateway": {"port": %d, "mode": "local"}
	}`, port)), 0o644); err != nil {
		t.Fatal(err)
	}

	bin := filepath.Join(tmpDir, "openclaw")
	if err := os.WriteFile(bin, []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$HOME/invocations.log\"\n"), 0o755); err != nil {
		t.Fatal(err)
	}

	oldCanInstallDaemon := openclawCanInstallDaemon
	openclawCanInstallDaemon = func() bool { return true }
	defer func() { openclawCanInstallDaemon = oldCanInstallDaemon }()

	triggeredBy := make(chan string, 1)
	listenerReady := make(chan net.Listener, 1)
	go func() {
		invocationsPath := filepath.Join(tmpDir, "invocations.log")
		deadline := time.Now().Add(5 * time.Second)
		for time.Now().Before(deadline) {
			data, err := os.ReadFile(invocationsPath)
			if err == nil {
				lines := strings.Split(strings.TrimSpace(string(data)), "\n")
				for _, line := range lines {
					if line != "daemon start" && line != "gateway run --force" {
						continue
					}
					ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
					if err != nil {
						return
					}
					go func() {
						for {
							conn, err := ln.Accept()
							if err != nil {
								return
							}
							_ = conn.Close()
						}
					}()
					triggeredBy <- line
					listenerReady <- ln
					return
				}
			}
			time.Sleep(10 * time.Millisecond)
		}
	}()

	c := &Openclaw{}
	cleanup, _, gotPort, err := c.ensureGatewayReady(bin)
	if err != nil {
		t.Fatalf("ensureGatewayReady() error = %v", err)
	}
	defer cleanup()
	if gotPort != port {
		t.Fatalf("ensureGatewayReady() port = %d, want %d", gotPort, port)
	}

	var ln net.Listener
	select {
	case which := <-triggeredBy:
		if which != "daemon start" {
			t.Fatalf("expected daemon start fallback, got %q", which)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for gateway startup trigger")
	}
	select {
	case ln = <-listenerReady:
		defer ln.Close()
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for test listener")
	}

	data, err := os.ReadFile(filepath.Join(tmpDir, "invocations.log"))
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	if len(lines) == 0 || lines[0] != "daemon start" {
		t.Fatalf("expected daemon start invocation, got %v", lines)
	}
	for _, line := range lines {
		if line == "gateway run --force" {
			t.Fatalf("did not expect gateway run fallback when daemon start succeeds, got %v", lines)
		}
	}
}

func TestOpenclawEnv_StagesBundledPluginRuntimeDeps(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	t.Setenv("OPENAI_API_KEY", "should-be-cleared")

	env := envSliceToMap(openclawEnv())

	if env["OPENCLAW_PLUGIN_STAGE_DIR"] != filepath.Join(tmpDir, ".openclaw", "plugin-runtime-deps") {
		t.Fatalf("OPENCLAW_PLUGIN_STAGE_DIR = %q, want %q", env["OPENCLAW_PLUGIN_STAGE_DIR"], filepath.Join(tmpDir, ".openclaw", "plugin-runtime-deps"))
	}
	if _, ok := env["OPENAI_API_KEY"]; ok {
		t.Fatal("expected OPENAI_API_KEY to be cleared from openclaw environment")
	}
}

func TestOpenclawInstallEnv_PreservesExplicitStageDirAndAddsEagerDeps(t *testing.T) {
	t.Setenv("OPENCLAW_PLUGIN_STAGE_DIR", "/tmp/custom-stage")

	env := envSliceToMap(openclawInstallEnv())

	if env["OPENCLAW_PLUGIN_STAGE_DIR"] != "/tmp/custom-stage" {
		t.Fatalf("OPENCLAW_PLUGIN_STAGE_DIR = %q, want %q", env["OPENCLAW_PLUGIN_STAGE_DIR"], "/tmp/custom-stage")
	}
	if env["OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS"] != "1" {
		t.Fatalf("OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS = %q, want %q", env["OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS"], "1")
	}
}

func TestEnsureOpenclawInstalled_UsesBundledPluginInstallEnv(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("uses a POSIX shell test binary")
	}

	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	t.Setenv("PATH", tmpDir)

	writeScript := func(path, content string) {
		t.Helper()
		if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
			t.Fatal(err)
		}
	}

	openclawPath := filepath.Join(tmpDir, "openclaw")
	npmScript := fmt.Sprintf(`#!/bin/sh
/usr/bin/env | /usr/bin/sort > "$HOME/npm-env.log"
/bin/cat > %q <<'EOF'
#!/bin/sh
exit 0
EOF
/bin/chmod +x %q
exit 0
`, openclawPath, openclawPath)
	writeScript(filepath.Join(tmpDir, "npm"), npmScript)
	writeScript(filepath.Join(tmpDir, "git"), "#!/bin/sh\nexit 0\n")

	oldConfirmPrompt := DefaultConfirmPrompt
	DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
		if prompt != "OpenClaw is not installed. Install with npm?" {
			t.Fatalf("unexpected prompt: %q", prompt)
		}
		return true, nil
	}
	defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

	openclawFreshInstall = false
	bin, err := ensureOpenclawInstalled()
	if err != nil {
		t.Fatalf("ensureOpenclawInstalled() error = %v", err)
	}
	if bin != "openclaw" {
		t.Fatalf("ensureOpenclawInstalled() bin = %q, want %q", bin, "openclaw")
	}

	envData, err := os.ReadFile(filepath.Join(tmpDir, "npm-env.log"))
	if err != nil {
		t.Fatal(err)
	}
	env := envSliceToMap(strings.Split(strings.TrimSpace(string(envData)), "\n"))
	if env["OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS"] != "1" {
		t.Fatalf("OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS = %q, want %q", env["OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS"], "1")
	}
	if env["OPENCLAW_PLUGIN_STAGE_DIR"] != filepath.Join(tmpDir, ".openclaw", "plugin-runtime-deps") {
		t.Fatalf("OPENCLAW_PLUGIN_STAGE_DIR = %q, want %q", env["OPENCLAW_PLUGIN_STAGE_DIR"], filepath.Join(tmpDir, ".openclaw", "plugin-runtime-deps"))
	}
}

func TestOpenclawEdit(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	configDir := filepath.Join(tmpDir, ".openclaw")
	configPath := filepath.Join(configDir, "openclaw.json")

	cleanup := func() { os.RemoveAll(configDir) }

	t.Run("fresh install", func(t *testing.T) {
		cleanup()
		if err := c.Edit(testLaunchModels("llama3.2")); err != nil {
			t.Fatal(err)
		}
		assertOpenclawModelExists(t, configPath, "llama3.2")
		assertOpenclawPrimaryModel(t, configPath, "ollama/llama3.2")
	})

	t.Run("multiple models - first is primary", func(t *testing.T) {
		cleanup()
		if err := c.Edit(testLaunchModels("llama3.2", "mistral")); err != nil {
			t.Fatal(err)
		}
		assertOpenclawModelExists(t, configPath, "llama3.2")
		assertOpenclawModelExists(t, configPath, "mistral")
		assertOpenclawPrimaryModel(t, configPath, "ollama/llama3.2")
	})

	t.Run("preserve other providers", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"models":{"providers":{"anthropic":{"apiKey":"xxx"}}}}`), 0o644)
		if err := c.Edit(testLaunchModels("llama3.2")); err != nil {
			t.Fatal(err)
		}
		data, _ := os.ReadFile(configPath)
		var cfg map[string]any
		json.Unmarshal(data, &cfg)
		models := cfg["models"].(map[string]any)
		providers := models["providers"].(map[string]any)
		if providers["anthropic"] == nil {
			t.Error("anthropic provider was removed")
		}
	})

	t.Run("preserve top-level keys", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"theme":"dark","mcp":{"servers":{}}}`), 0o644)
		if err := c.Edit(testLaunchModels("llama3.2")); err != nil {
			t.Fatal(err)
		}
		data, _ := os.ReadFile(configPath)
		var cfg map[string]any
		json.Unmarshal(data, &cfg)
		if cfg["theme"] != "dark" {
			t.Error("theme was removed")
		}
		if cfg["mcp"] == nil {
			t.Error("mcp was removed")
		}
	})

	t.Run("preserve user customizations on models", func(t *testing.T) {
		cleanup()
		c.Edit(testLaunchModels("llama3.2"))

		// User adds custom field
		data, _ := os.ReadFile(configPath)
		var cfg map[string]any
		json.Unmarshal(data, &cfg)
		models := cfg["models"].(map[string]any)
		providers := models["providers"].(map[string]any)
		ollama := providers["ollama"].(map[string]any)
		modelList := ollama["models"].([]any)
		entry := modelList[0].(map[string]any)
		entry["customField"] = "user-value"
		configData, _ := json.MarshalIndent(cfg, "", "  ")
		os.WriteFile(configPath, configData, 0o644)

		// Re-run Edit
		c.Edit(testLaunchModels("llama3.2"))

		data, _ = os.ReadFile(configPath)
		json.Unmarshal(data, &cfg)
		models = cfg["models"].(map[string]any)
		providers = models["providers"].(map[string]any)
		ollama = providers["ollama"].(map[string]any)
		modelList = ollama["models"].([]any)
		entry = modelList[0].(map[string]any)
		if entry["customField"] != "user-value" {
			t.Error("custom field was lost")
		}
	})

	t.Run("edit replaces models list", func(t *testing.T) {
		cleanup()
		c.Edit(testLaunchModels("llama3.2", "mistral"))
		c.Edit(testLaunchModels("llama3.2"))

		assertOpenclawModelExists(t, configPath, "llama3.2")
		assertOpenclawModelNotExists(t, configPath, "mistral")
	})

	t.Run("empty models is no-op", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		original := `{"existing":"data"}`
		os.WriteFile(configPath, []byte(original), 0o644)

		c.Edit(testLaunchModels())

		data, _ := os.ReadFile(configPath)
		if string(data) != original {
			t.Error("empty models should not modify file")
		}
	})

	t.Run("corrupted JSON treated as empty", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{corrupted`), 0o644)

		if err := c.Edit(testLaunchModels("llama3.2")); err != nil {
			t.Fatal(err)
		}

		data, _ := os.ReadFile(configPath)
		var cfg map[string]any
		if err := json.Unmarshal(data, &cfg); err != nil {
			t.Error("result should be valid JSON")
		}
	})

	t.Run("wrong type models section", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"models":"not a map"}`), 0o644)

		if err := c.Edit(testLaunchModels("llama3.2")); err != nil {
			t.Fatal(err)
		}
		assertOpenclawModelExists(t, configPath, "llama3.2")
	})
}

func TestOpenclawModels(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	t.Run("no config returns nil", func(t *testing.T) {
		if models := c.Models(); len(models) > 0 {
			t.Errorf("expected nil/empty, got %v", models)
		}
	})

	t.Run("returns all ollama models", func(t *testing.T) {
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{
			"models":{"providers":{"ollama":{"models":[
				{"id":"llama3.2"},
				{"id":"mistral"}
			]}}}
		}`), 0o644)

		models := c.Models()
		if len(models) != 2 {
			t.Errorf("expected 2 models, got %v", models)
		}
	})
}

// Helper functions
func assertOpenclawModelExists(t *testing.T, path, model string) {
	t.Helper()
	data, _ := os.ReadFile(path)
	var cfg map[string]any
	json.Unmarshal(data, &cfg)
	models := cfg["models"].(map[string]any)
	providers := models["providers"].(map[string]any)
	ollama := providers["ollama"].(map[string]any)
	modelList := ollama["models"].([]any)
	for _, m := range modelList {
		if entry, ok := m.(map[string]any); ok {
			if entry["id"] == model {
				return
			}
		}
	}
	t.Errorf("model %s not found", model)
}

func assertOpenclawModelNotExists(t *testing.T, path, model string) {
	t.Helper()
	data, _ := os.ReadFile(path)
	var cfg map[string]any
	json.Unmarshal(data, &cfg)
	models, _ := cfg["models"].(map[string]any)
	providers, _ := models["providers"].(map[string]any)
	ollama, _ := providers["ollama"].(map[string]any)
	modelList, _ := ollama["models"].([]any)
	for _, m := range modelList {
		if entry, ok := m.(map[string]any); ok {
			if entry["id"] == model {
				t.Errorf("model %s should not exist", model)
			}
		}
	}
}

func assertOpenclawPrimaryModel(t *testing.T, path, expected string) {
	t.Helper()
	data, _ := os.ReadFile(path)
	var cfg map[string]any
	json.Unmarshal(data, &cfg)
	agents := cfg["agents"].(map[string]any)
	defaults := agents["defaults"].(map[string]any)
	model := defaults["model"].(map[string]any)
	if model["primary"] != expected {
		t.Errorf("primary model = %v, want %v", model["primary"], expected)
	}
}

func TestOpenclawPaths(t *testing.T) {
	c := &Openclaw{}

	t.Run("returns path when config exists", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{}`), 0o644)

		paths := c.Paths()
		if len(paths) != 1 {
			t.Errorf("expected 1 path, got %d", len(paths))
		}
	})

	t.Run("returns nil when config missing", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		if paths := c.Paths(); paths != nil {
			t.Errorf("expected nil, got %v", paths)
		}
	})
}

func TestOpenclawModelsEdgeCases(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	configDir := filepath.Join(tmpDir, ".openclaw")
	configPath := filepath.Join(configDir, "openclaw.json")
	cleanup := func() { os.RemoveAll(configDir) }

	t.Run("corrupted JSON returns nil", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{corrupted`), 0o644)
		if models := c.Models(); models != nil {
			t.Errorf("expected nil, got %v", models)
		}
	})

	t.Run("wrong type at models level", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"models":"string"}`), 0o644)
		if models := c.Models(); models != nil {
			t.Errorf("expected nil, got %v", models)
		}
	})

	t.Run("wrong type at providers level", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"models":{"providers":"string"}}`), 0o644)
		if models := c.Models(); models != nil {
			t.Errorf("expected nil, got %v", models)
		}
	})

	t.Run("wrong type at ollama level", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"models":{"providers":{"ollama":"string"}}}`), 0o644)
		if models := c.Models(); models != nil {
			t.Errorf("expected nil, got %v", models)
		}
	})

	t.Run("model entry missing id", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"models":{"providers":{"ollama":{"models":[{"name":"test"}]}}}}`), 0o644)
		if len(c.Models()) != 0 {
			t.Error("expected empty for missing id")
		}
	})

	t.Run("model id is not string", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"models":{"providers":{"ollama":{"models":[{"id":123}]}}}}`), 0o644)
		if len(c.Models()) != 0 {
			t.Error("expected empty for non-string id")
		}
	})
}

func TestOpenclawEditSchemaFields(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	configPath := filepath.Join(tmpDir, ".openclaw", "openclaw.json")

	if err := c.Edit(testLaunchModels("llama3.2")); err != nil {
		t.Fatal(err)
	}

	data, _ := os.ReadFile(configPath)
	var cfg map[string]any
	json.Unmarshal(data, &cfg)
	models := cfg["models"].(map[string]any)
	providers := models["providers"].(map[string]any)
	ollama := providers["ollama"].(map[string]any)
	modelList := ollama["models"].([]any)
	entry := modelList[0].(map[string]any)

	// Verify base schema fields (always set regardless of API availability)
	if entry["id"] != "llama3.2" {
		t.Errorf("id = %v, want llama3.2", entry["id"])
	}
	if entry["name"] != "llama3.2" {
		t.Errorf("name = %v, want llama3.2", entry["name"])
	}
	if entry["input"] == nil {
		t.Error("input should be set")
	}
	cost := entry["cost"].(map[string]any)
	if cost["cacheRead"] == nil {
		t.Error("cost.cacheRead should be set")
	}
	if cost["cacheWrite"] == nil {
		t.Error("cost.cacheWrite should be set")
	}
}

func TestOpenclawEditModelNames(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	configPath := filepath.Join(tmpDir, ".openclaw", "openclaw.json")
	cleanup := func() { os.RemoveAll(filepath.Join(tmpDir, ".openclaw")) }

	t.Run("model with colon tag", func(t *testing.T) {
		cleanup()
		if err := c.Edit(testLaunchModels("llama3.2:70b")); err != nil {
			t.Fatal(err)
		}
		assertOpenclawModelExists(t, configPath, "llama3.2:70b")
		assertOpenclawPrimaryModel(t, configPath, "ollama/llama3.2:70b")
	})

	t.Run("model with slash", func(t *testing.T) {
		cleanup()
		if err := c.Edit(testLaunchModels("library/model:tag")); err != nil {
			t.Fatal(err)
		}
		assertOpenclawModelExists(t, configPath, "library/model:tag")
		assertOpenclawPrimaryModel(t, configPath, "ollama/library/model:tag")
	})

	t.Run("model with hyphen", func(t *testing.T) {
		cleanup()
		if err := c.Edit(testLaunchModels("test-model")); err != nil {
			t.Fatal(err)
		}
		assertOpenclawModelExists(t, configPath, "test-model")
	})
}

func TestOpenclawEditAgentsPreservation(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	configDir := filepath.Join(tmpDir, ".openclaw")
	configPath := filepath.Join(configDir, "openclaw.json")
	cleanup := func() { os.RemoveAll(configDir) }

	t.Run("preserve other agent defaults", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"agents":{"defaults":{"model":{"primary":"old"},"temperature":0.7}}}`), 0o644)

		c.Edit(testLaunchModels("llama3.2"))

		data, _ := os.ReadFile(configPath)
		var cfg map[string]any
		json.Unmarshal(data, &cfg)
		agents := cfg["agents"].(map[string]any)
		defaults := agents["defaults"].(map[string]any)
		if defaults["temperature"] != 0.7 {
			t.Error("temperature setting was lost")
		}
	})

	t.Run("preserve other agents besides defaults", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"agents":{"defaults":{},"custom-agent":{"foo":"bar"}}}`), 0o644)

		c.Edit(testLaunchModels("llama3.2"))

		data, _ := os.ReadFile(configPath)
		var cfg map[string]any
		json.Unmarshal(data, &cfg)
		agents := cfg["agents"].(map[string]any)
		if agents["custom-agent"] == nil {
			t.Error("custom-agent was lost")
		}
	})
}

const testOpenclawFixture = `{
  "theme": "dark",
  "mcp": {"servers": {"custom": {"enabled": true}}},
  "models": {
    "providers": {
      "anthropic": {"apiKey": "xxx"},
      "ollama": {
        "baseUrl": "http://127.0.0.1:11434",
        "models": [{"id": "old-model", "customField": "preserved"}]
      }
    }
  },
  "agents": {
    "defaults": {"model": {"primary": "old"}, "temperature": 0.7},
    "custom-agent": {"foo": "bar"}
  }
}`

func TestOpenclawEdit_RoundTrip(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	configDir := filepath.Join(tmpDir, ".openclaw")
	configPath := filepath.Join(configDir, "openclaw.json")

	os.MkdirAll(configDir, 0o755)
	os.WriteFile(configPath, []byte(testOpenclawFixture), 0o644)

	if err := c.Edit(testLaunchModels("llama3.2", "mistral")); err != nil {
		t.Fatal(err)
	}

	data, _ := os.ReadFile(configPath)
	var cfg map[string]any
	json.Unmarshal(data, &cfg)

	// Verify top-level preserved
	if cfg["theme"] != "dark" {
		t.Error("theme not preserved")
	}
	mcp := cfg["mcp"].(map[string]any)
	servers := mcp["servers"].(map[string]any)
	if servers["custom"] == nil {
		t.Error("mcp.servers.custom not preserved")
	}

	// Verify other providers preserved
	models := cfg["models"].(map[string]any)
	providers := models["providers"].(map[string]any)
	if providers["anthropic"] == nil {
		t.Error("anthropic provider not preserved")
	}

	// Verify agents preserved
	agents := cfg["agents"].(map[string]any)
	if agents["custom-agent"] == nil {
		t.Error("custom-agent not preserved")
	}
	defaults := agents["defaults"].(map[string]any)
	if defaults["temperature"] != 0.7 {
		t.Error("temperature not preserved")
	}
}

func TestOpenclawEdit_Idempotent(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	configDir := filepath.Join(tmpDir, ".openclaw")
	configPath := filepath.Join(configDir, "openclaw.json")

	os.MkdirAll(configDir, 0o755)
	os.WriteFile(configPath, []byte(testOpenclawFixture), 0o644)

	c.Edit(testLaunchModels("llama3.2", "mistral"))
	firstData, _ := os.ReadFile(configPath)

	c.Edit(testLaunchModels("llama3.2", "mistral"))
	secondData, _ := os.ReadFile(configPath)

	if string(firstData) != string(secondData) {
		t.Error("repeated edits with same models produced different results")
	}
}

func TestOpenclawEdit_MultipleConsecutiveEdits(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	configDir := filepath.Join(tmpDir, ".openclaw")
	configPath := filepath.Join(configDir, "openclaw.json")

	os.MkdirAll(configDir, 0o755)
	os.WriteFile(configPath, []byte(testOpenclawFixture), 0o644)

	for i := range 10 {
		models := []string{"model-a", "model-b"}
		if i%2 == 0 {
			models = []string{"model-x", "model-y", "model-z"}
		}
		if err := c.Edit(launchModelsFromNames(models)); err != nil {
			t.Fatalf("edit %d failed: %v", i, err)
		}
	}

	data, _ := os.ReadFile(configPath)
	var cfg map[string]any
	if err := json.Unmarshal(data, &cfg); err != nil {
		t.Fatalf("file is not valid JSON after multiple edits: %v", err)
	}

	if cfg["theme"] != "dark" {
		t.Error("theme lost after multiple edits")
	}
}

func TestOpenclawEdit_BackupCreated(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	configDir := filepath.Join(tmpDir, ".openclaw")
	configPath := filepath.Join(configDir, "openclaw.json")
	backupDir := fileutil.BackupDir()

	os.MkdirAll(configDir, 0o755)
	uniqueMarker := fmt.Sprintf("test-marker-%d", os.Getpid())
	original := fmt.Sprintf(`{"theme": "%s"}`, uniqueMarker)
	os.WriteFile(configPath, []byte(original), 0o644)

	if err := c.Edit(testLaunchModels("model-a")); err != nil {
		t.Fatal(err)
	}

	backups, _ := filepath.Glob(filepath.Join(backupDir, "openclaw", "openclaw.json.*"))
	foundBackup := false
	for _, backup := range backups {
		data, _ := os.ReadFile(backup)
		if string(data) == original {
			foundBackup = true
			break
		}
	}

	if !foundBackup {
		t.Error("backup with original content not found")
	}
}

func TestOpenclawClawdbotAlias(t *testing.T) {
	for _, alias := range []string{"clawdbot", "moltbot"} {
		t.Run(alias+" alias resolves to Openclaw runner", func(t *testing.T) {
			r, ok := integrations[alias]
			if !ok {
				t.Fatalf("%s not found in integrations", alias)
			}
			if _, ok := r.(*Openclaw); !ok {
				t.Errorf("%s integration is %T, want *Openclaw", alias, r)
			}
		})

		t.Run(alias+" is hidden from selector", func(t *testing.T) {
			if !integrationAliases[alias] {
				t.Errorf("%s should be in integrationAliases", alias)
			}
		})
	}
}

func TestOpenclawLegacyPaths(t *testing.T) {
	c := &Openclaw{}

	t.Run("falls back to legacy clawdbot path", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		legacyDir := filepath.Join(tmpDir, ".clawdbot")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{}`), 0o644)

		paths := c.Paths()
		if len(paths) != 1 {
			t.Fatalf("expected 1 path, got %d", len(paths))
		}
		if paths[0] != filepath.Join(legacyDir, "clawdbot.json") {
			t.Errorf("expected legacy path, got %s", paths[0])
		}
	})

	t.Run("prefers new path over legacy", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		newDir := filepath.Join(tmpDir, ".openclaw")
		legacyDir := filepath.Join(tmpDir, ".clawdbot")
		os.MkdirAll(newDir, 0o755)
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(newDir, "openclaw.json"), []byte(`{}`), 0o644)
		os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{}`), 0o644)

		paths := c.Paths()
		if len(paths) != 1 {
			t.Fatalf("expected 1 path, got %d", len(paths))
		}
		if paths[0] != filepath.Join(newDir, "openclaw.json") {
			t.Errorf("expected new path, got %s", paths[0])
		}
	})

	t.Run("Models reads from legacy path", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		legacyDir := filepath.Join(tmpDir, ".clawdbot")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{
			"models":{"providers":{"ollama":{"models":[{"id":"llama3.2"}]}}}
		}`), 0o644)

		models := c.Models()
		if len(models) != 1 || models[0] != "llama3.2" {
			t.Errorf("expected [llama3.2], got %v", models)
		}
	})

	t.Run("Models prefers new path over legacy", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		newDir := filepath.Join(tmpDir, ".openclaw")
		legacyDir := filepath.Join(tmpDir, ".clawdbot")
		os.MkdirAll(newDir, 0o755)
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(newDir, "openclaw.json"), []byte(`{
			"models":{"providers":{"ollama":{"models":[{"id":"new-model"}]}}}
		}`), 0o644)
		os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{
			"models":{"providers":{"ollama":{"models":[{"id":"legacy-model"}]}}}
		}`), 0o644)

		models := c.Models()
		if len(models) != 1 || models[0] != "new-model" {
			t.Errorf("expected [new-model], got %v", models)
		}
	})

	t.Run("Edit reads new path over legacy when both exist", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		newDir := filepath.Join(tmpDir, ".openclaw")
		legacyDir := filepath.Join(tmpDir, ".clawdbot")
		os.MkdirAll(newDir, 0o755)
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(newDir, "openclaw.json"), []byte(`{"theme":"new"}`), 0o644)
		os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{"theme":"legacy"}`), 0o644)

		if err := c.Edit(testLaunchModels("llama3.2")); err != nil {
			t.Fatal(err)
		}

		data, _ := os.ReadFile(filepath.Join(newDir, "openclaw.json"))
		var cfg map[string]any
		json.Unmarshal(data, &cfg)
		if cfg["theme"] != "new" {
			t.Errorf("expected theme from new config, got %v", cfg["theme"])
		}
	})

	t.Run("Edit migrates from legacy config", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		legacyDir := filepath.Join(tmpDir, ".clawdbot")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{"theme":"dark"}`), 0o644)

		if err := c.Edit(testLaunchModels("llama3.2")); err != nil {
			t.Fatal(err)
		}

		// Should write to new path
		newPath := filepath.Join(tmpDir, ".openclaw", "openclaw.json")
		data, err := os.ReadFile(newPath)
		if err != nil {
			t.Fatal("expected new config file to be created")
		}
		var cfg map[string]any
		json.Unmarshal(data, &cfg)
		if cfg["theme"] != "dark" {
			t.Error("legacy theme setting was not migrated")
		}
	})
}

func TestOpenclawEdit_CreatesDirectoryIfMissing(t *testing.T) {
	c := &Openclaw{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	configDir := filepath.Join(tmpDir, ".openclaw")

	if _, err := os.Stat(configDir); !os.IsNotExist(err) {
		t.Fatal("directory should not exist before test")
	}

	if err := c.Edit(testLaunchModels("model-a")); err != nil {
		t.Fatal(err)
	}

	if _, err := os.Stat(configDir); os.IsNotExist(err) {
		t.Fatal("directory was not created")
	}
}

func TestOpenclawOnboarded(t *testing.T) {
	c := &Openclaw{}

	t.Run("returns false when no config exists", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		if c.onboarded() {
			t.Error("expected false when no config exists")
		}
	})

	t.Run("returns false when config exists but no wizard section", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"theme":"dark"}`), 0o644)

		if c.onboarded() {
			t.Error("expected false when no wizard section")
		}
	})

	t.Run("returns false when wizard section exists but no lastRunAt", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"wizard":{}}`), 0o644)

		if c.onboarded() {
			t.Error("expected false when wizard.lastRunAt is missing")
		}
	})

	t.Run("returns false when wizard.lastRunAt is empty string", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"wizard":{"lastRunAt":""}}`), 0o644)

		if c.onboarded() {
			t.Error("expected false when wizard.lastRunAt is empty")
		}
	})

	t.Run("returns true when wizard.lastRunAt is set", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"wizard":{"lastRunAt":"2024-01-01T00:00:00Z"}}`), 0o644)

		if !c.onboarded() {
			t.Error("expected true when wizard.lastRunAt is set")
		}
	})

	t.Run("checks legacy clawdbot path", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		legacyDir := filepath.Join(tmpDir, ".clawdbot")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{"wizard":{"lastRunAt":"2024-01-01T00:00:00Z"}}`), 0o644)

		if !c.onboarded() {
			t.Error("expected true when legacy config has wizard.lastRunAt")
		}
	})

	t.Run("prefers new path over legacy", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		newDir := filepath.Join(tmpDir, ".openclaw")
		legacyDir := filepath.Join(tmpDir, ".clawdbot")
		os.MkdirAll(newDir, 0o755)
		os.MkdirAll(legacyDir, 0o755)
		// New path has no wizard marker
		os.WriteFile(filepath.Join(newDir, "openclaw.json"), []byte(`{}`), 0o644)
		// Legacy has wizard marker
		os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{"wizard":{"lastRunAt":"2024-01-01T00:00:00Z"}}`), 0o644)

		if c.onboarded() {
			t.Error("expected false - should prefer new path which has no wizard marker")
		}
	})

	t.Run("handles corrupted JSON gracefully", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{corrupted`), 0o644)

		if c.onboarded() {
			t.Error("expected false for corrupted JSON")
		}
	})

	t.Run("handles wrong type for wizard section", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"wizard":"not a map"}`), 0o644)

		if c.onboarded() {
			t.Error("expected false when wizard is wrong type")
		}
	})
}

func TestOpenclawChannelsConfigured(t *testing.T) {
	c := &Openclaw{}

	t.Run("returns false when no config exists", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		if c.channelsConfigured() {
			t.Error("expected false when no config exists")
		}
	})

	t.Run("returns false for corrupted json", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{bad`), 0o644); err != nil {
			t.Fatal(err)
		}

		if c.channelsConfigured() {
			t.Error("expected false for corrupted config")
		}
	})

	t.Run("returns false when channels section is missing", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"theme":"dark"}`), 0o644); err != nil {
			t.Fatal(err)
		}

		if c.channelsConfigured() {
			t.Error("expected false when channels section is missing")
		}
	})

	t.Run("returns false for channels defaults and modelByChannel only", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{
			"channels": {
				"defaults": {"dmPolicy": "pairing"},
				"modelByChannel": {"telegram": "ollama/llama3.2"}
			}
		}`), 0o644); err != nil {
			t.Fatal(err)
		}

		if c.channelsConfigured() {
			t.Error("expected false for channels metadata only")
		}
	})

	t.Run("returns false when channel entry only has enabled", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{
			"channels": {
				"telegram": {"enabled": true}
			}
		}`), 0o644); err != nil {
			t.Fatal(err)
		}

		if c.channelsConfigured() {
			t.Error("expected false when channel config only has enabled")
		}
	})

	t.Run("returns true when a channel has meaningful configuration", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{
			"channels": {
				"telegram": {"botToken": "secret"}
			}
		}`), 0o644); err != nil {
			t.Fatal(err)
		}

		if !c.channelsConfigured() {
			t.Error("expected true when channel has meaningful config")
		}
	})

	t.Run("prefers new path over legacy", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		newDir := filepath.Join(tmpDir, ".openclaw")
		legacyDir := filepath.Join(tmpDir, ".clawdbot")
		if err := os.MkdirAll(newDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.MkdirAll(legacyDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(newDir, "openclaw.json"), []byte(`{"channels":{"telegram":{"enabled":true}}}`), 0o644); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{"channels":{"telegram":{"botToken":"configured"}}}`), 0o644); err != nil {
			t.Fatal(err)
		}

		if c.channelsConfigured() {
			t.Error("expected false because new config should take precedence")
		}
	})
}

func envSliceToMap(entries []string) map[string]string {
	env := make(map[string]string, len(entries))
	for _, entry := range entries {
		key, value, ok := strings.Cut(entry, "=")
		if !ok {
			continue
		}
		env[key] = value
	}
	return env
}

func TestOpenclawChannelSetupPreflight(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("uses a POSIX shell test binary")
	}

	c := &Openclaw{}

	t.Run("skips in non-interactive sessions", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		t.Setenv("PATH", tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{}`), 0o644); err != nil {
			t.Fatal(err)
		}
		bin := filepath.Join(tmpDir, "openclaw")
		if err := os.WriteFile(bin, []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$HOME/invocations.log\"\n"), 0o755); err != nil {
			t.Fatal(err)
		}

		oldInteractive := isInteractiveSession
		isInteractiveSession = func() bool { return false }
		defer func() { isInteractiveSession = oldInteractive }()

		oldConfirmPrompt := DefaultConfirmPrompt
		DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
			t.Fatalf("did not expect prompt in non-interactive mode: %s", prompt)
			return false, nil
		}
		defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

		if err := c.runChannelSetupPreflight("openclaw"); err != nil {
			t.Fatalf("runChannelSetupPreflight() error = %v", err)
		}
		if _, err := os.Stat(filepath.Join(tmpDir, "invocations.log")); !os.IsNotExist(err) {
			t.Fatalf("expected no command invocation in non-interactive mode, got err=%v", err)
		}
	})

	t.Run("already configured does not prompt or run channels add", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		t.Setenv("PATH", tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"channels":{"telegram":{"botToken":"set"}}}`), 0o644); err != nil {
			t.Fatal(err)
		}
		bin := filepath.Join(tmpDir, "openclaw")
		if err := os.WriteFile(bin, []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$HOME/invocations.log\"\n"), 0o755); err != nil {
			t.Fatal(err)
		}

		oldInteractive := isInteractiveSession
		isInteractiveSession = func() bool { return true }
		defer func() { isInteractiveSession = oldInteractive }()

		oldConfirmPrompt := DefaultConfirmPrompt
		DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
			t.Fatalf("did not expect prompt when already configured: %s", prompt)
			return false, nil
		}
		defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

		if err := c.runChannelSetupPreflight("openclaw"); err != nil {
			t.Fatalf("runChannelSetupPreflight() error = %v", err)
		}
		if _, err := os.Stat(filepath.Join(tmpDir, "invocations.log")); !os.IsNotExist(err) {
			t.Fatalf("expected no channels add invocation, got err=%v", err)
		}
	})

	t.Run("--yes skips preflight without channels configured", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		t.Setenv("PATH", tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		// Empty config = no channels configured. Without the --yes skip, the
		// preflight would prompt and (on confirm) spawn `openclaw channels add`.
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{}`), 0o644); err != nil {
			t.Fatal(err)
		}
		bin := filepath.Join(tmpDir, "openclaw")
		if err := os.WriteFile(bin, []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$HOME/invocations.log\"\n"), 0o755); err != nil {
			t.Fatal(err)
		}

		oldInteractive := isInteractiveSession
		isInteractiveSession = func() bool { return true }
		defer func() { isInteractiveSession = oldInteractive }()

		restore := withLaunchConfirmPolicy(launchConfirmPolicy{yes: true})
		defer restore()

		oldConfirmPrompt := DefaultConfirmPrompt
		DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
			t.Fatalf("did not expect prompt in --yes mode: %s", prompt)
			return false, nil
		}
		defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

		if err := c.runChannelSetupPreflight("openclaw"); err != nil {
			t.Fatalf("runChannelSetupPreflight() error = %v", err)
		}
		if _, err := os.Stat(filepath.Join(tmpDir, "invocations.log")); !os.IsNotExist(err) {
			t.Fatalf("expected no channels add invocation in --yes mode, got err=%v", err)
		}
	})

	t.Run("set up later prompts once and exits", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		t.Setenv("PATH", tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{}`), 0o644); err != nil {
			t.Fatal(err)
		}
		bin := filepath.Join(tmpDir, "openclaw")
		if err := os.WriteFile(bin, []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$HOME/invocations.log\"\n"), 0o755); err != nil {
			t.Fatal(err)
		}

		oldInteractive := isInteractiveSession
		isInteractiveSession = func() bool { return true }
		defer func() { isInteractiveSession = oldInteractive }()

		promptCount := 0
		oldConfirmPrompt := DefaultConfirmPrompt
		DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
			promptCount++
			return false, nil
		}
		defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

		if err := c.runChannelSetupPreflight("openclaw"); err != nil {
			t.Fatalf("runChannelSetupPreflight() error = %v", err)
		}
		if promptCount != 1 {
			t.Fatalf("expected 1 prompt, got %d", promptCount)
		}
		if _, err := os.Stat(filepath.Join(tmpDir, "invocations.log")); !os.IsNotExist(err) {
			t.Fatalf("expected no channels add invocation, got err=%v", err)
		}
	})

	t.Run("yes runs channels add and exits after configuration", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		t.Setenv("PATH", tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{}`), 0o644); err != nil {
			t.Fatal(err)
		}
		bin := filepath.Join(tmpDir, "openclaw")
		script := `#!/bin/sh
printf '%s\n' "$*" >> "$HOME/invocations.log"
if [ "$1" = "channels" ] && [ "$2" = "add" ]; then
  /bin/mkdir -p "$HOME/.openclaw"
  /bin/cat > "$HOME/.openclaw/openclaw.json" <<'EOF'
{"channels":{"telegram":{"botToken":"configured"}}}
EOF
fi
`
		if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
			t.Fatal(err)
		}

		oldInteractive := isInteractiveSession
		isInteractiveSession = func() bool { return true }
		defer func() { isInteractiveSession = oldInteractive }()

		promptCount := 0
		oldConfirmPrompt := DefaultConfirmPrompt
		DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
			promptCount++
			return true, nil
		}
		defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

		if err := c.runChannelSetupPreflight("openclaw"); err != nil {
			t.Fatalf("runChannelSetupPreflight() error = %v", err)
		}
		if promptCount != 1 {
			t.Fatalf("expected 1 prompt, got %d", promptCount)
		}

		data, err := os.ReadFile(filepath.Join(tmpDir, "invocations.log"))
		if err != nil {
			t.Fatal(err)
		}
		lines := strings.Split(strings.TrimSpace(string(data)), "\n")
		if len(lines) != 1 || lines[0] != "channels add" {
			t.Fatalf("expected one 'channels add' invocation, got %v", lines)
		}
	})

	t.Run("re-prompts when channels add does not configure anything", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		t.Setenv("PATH", tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{}`), 0o644); err != nil {
			t.Fatal(err)
		}
		bin := filepath.Join(tmpDir, "openclaw")
		if err := os.WriteFile(bin, []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$HOME/invocations.log\"\n"), 0o755); err != nil {
			t.Fatal(err)
		}

		oldInteractive := isInteractiveSession
		isInteractiveSession = func() bool { return true }
		defer func() { isInteractiveSession = oldInteractive }()

		promptCount := 0
		oldConfirmPrompt := DefaultConfirmPrompt
		DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
			promptCount++
			return promptCount == 1, nil
		}
		defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

		if err := c.runChannelSetupPreflight("openclaw"); err != nil {
			t.Fatalf("runChannelSetupPreflight() error = %v", err)
		}
		if promptCount != 2 {
			t.Fatalf("expected 2 prompts, got %d", promptCount)
		}

		data, err := os.ReadFile(filepath.Join(tmpDir, "invocations.log"))
		if err != nil {
			t.Fatal(err)
		}
		lines := strings.Split(strings.TrimSpace(string(data)), "\n")
		if len(lines) != 1 || lines[0] != "channels add" {
			t.Fatalf("expected one 'channels add' invocation, got %v", lines)
		}
	})

	t.Run("returns actionable error when channels add fails", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		t.Setenv("PATH", tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		if err := os.MkdirAll(configDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{}`), 0o644); err != nil {
			t.Fatal(err)
		}
		bin := filepath.Join(tmpDir, "openclaw")
		script := `#!/bin/sh
if [ "$1" = "channels" ] && [ "$2" = "add" ]; then
  exit 42
fi
`
		if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
			t.Fatal(err)
		}

		oldInteractive := isInteractiveSession
		isInteractiveSession = func() bool { return true }
		defer func() { isInteractiveSession = oldInteractive }()

		oldConfirmPrompt := DefaultConfirmPrompt
		DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
			return true, nil
		}
		defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()

		err := c.runChannelSetupPreflight("openclaw")
		if err == nil {
			t.Fatal("expected error when channels add fails")
		}
		if !strings.Contains(err.Error(), "Try running: openclaw channels add") {
			t.Fatalf("expected actionable remediation hint, got: %v", err)
		}
	})
}

func TestOpenclawGatewayInfo(t *testing.T) {
	c := &Openclaw{}

	t.Run("returns defaults when no config exists", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		token, port := c.gatewayInfo()
		if token != "" {
			t.Errorf("expected empty token, got %q", token)
		}
		if port != defaultGatewayPort {
			t.Errorf("expected default port %d, got %d", defaultGatewayPort, port)
		}
	})

	t.Run("reads token and port from config", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{
			"gateway": {
				"port": 9999,
				"auth": {"mode": "token", "token": "my-secret"}
			}
		}`), 0o644)

		token, port := c.gatewayInfo()
		if token != "my-secret" {
			t.Errorf("expected token %q, got %q", "my-secret", token)
		}
		if port != 9999 {
			t.Errorf("expected port 9999, got %d", port)
		}
	})

	t.Run("uses default port when not in config", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{
			"gateway": {"auth": {"token": "tok"}}
		}`), 0o644)

		token, port := c.gatewayInfo()
		if token != "tok" {
			t.Errorf("expected token %q, got %q", "tok", token)
		}
		if port != defaultGatewayPort {
			t.Errorf("expected default port %d, got %d", defaultGatewayPort, port)
		}
	})

	t.Run("falls back to legacy clawdbot config", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		legacyDir := filepath.Join(tmpDir, ".clawdbot")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{
			"gateway": {"port": 12345, "auth": {"token": "legacy-token"}}
		}`), 0o644)

		token, port := c.gatewayInfo()
		if token != "legacy-token" {
			t.Errorf("expected token %q, got %q", "legacy-token", token)
		}
		if port != 12345 {
			t.Errorf("expected port 12345, got %d", port)
		}
	})

	t.Run("handles corrupted JSON gracefully", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{corrupted`), 0o644)

		token, port := c.gatewayInfo()
		if token != "" {
			t.Errorf("expected empty token, got %q", token)
		}
		if port != defaultGatewayPort {
			t.Errorf("expected default port, got %d", port)
		}
	})

	t.Run("handles missing gateway section", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		configDir := filepath.Join(tmpDir, ".openclaw")
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"theme":"dark"}`), 0o644)

		token, port := c.gatewayInfo()
		if token != "" {
			t.Errorf("expected empty token, got %q", token)
		}
		if port != defaultGatewayPort {
			t.Errorf("expected default port, got %d", port)
		}
	})
}

func TestPatchDeviceScopes(t *testing.T) {
	t.Run("patches device approved scopes and operator token only", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		identityDir := filepath.Join(tmpDir, ".openclaw", "identity")
		if err := os.MkdirAll(identityDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(identityDir, "device-auth.json"), []byte(`{"deviceId":"dev-1"}`), 0o600); err != nil {
			t.Fatal(err)
		}

		devicesDir := filepath.Join(tmpDir, ".openclaw", "devices")
		if err := os.MkdirAll(devicesDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(devicesDir, "paired.json"), []byte(`{
			"dev-1": {
				"deviceId": "dev-1",
				"scopes": ["operator.read"],
				"approvedScopes": ["operator.read"],
				"tokens": {
					"operator": {"role":"operator","scopes":["operator.read"]},
					"node": {"role":"node","scopes":["node.exec"]}
				}
			}
		}`), 0o600); err != nil {
			t.Fatal(err)
		}

		patchDeviceScopes()

		data, err := os.ReadFile(filepath.Join(devicesDir, "paired.json"))
		if err != nil {
			t.Fatal(err)
		}
		var devices map[string]map[string]any
		if err := json.Unmarshal(data, &devices); err != nil {
			t.Fatal(err)
		}

		required := []string{
			"operator.read",
			"operator.admin",
			"operator.approvals",
			"operator.pairing",
		}

		toSet := func(v any) map[string]bool {
			out := map[string]bool{}
			items, _ := v.([]any)
			for _, item := range items {
				if s, ok := item.(string); ok {
					out[s] = true
				}
			}
			return out
		}
		assertContainsAll := func(name string, got any, want []string) {
			t.Helper()
			set := toSet(got)
			for _, scope := range want {
				if !set[scope] {
					t.Fatalf("%s missing required scope %q (got=%v)", name, scope, set)
				}
			}
		}

		dev := devices["dev-1"]
		assertContainsAll("device.scopes", dev["scopes"], required)
		assertContainsAll("device.approvedScopes", dev["approvedScopes"], required)

		tokens, _ := dev["tokens"].(map[string]any)
		operator, _ := tokens["operator"].(map[string]any)
		assertContainsAll("tokens.operator.scopes", operator["scopes"], required)

		node, _ := tokens["node"].(map[string]any)
		nodeScopes := toSet(node["scopes"])
		if len(nodeScopes) != 1 || !nodeScopes["node.exec"] {
			t.Fatalf("expected non-operator token scopes unchanged, got=%v", nodeScopes)
		}
	})

	t.Run("creates approvedScopes when missing", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		identityDir := filepath.Join(tmpDir, ".openclaw", "identity")
		if err := os.MkdirAll(identityDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(identityDir, "device-auth.json"), []byte(`{"deviceId":"dev-2"}`), 0o600); err != nil {
			t.Fatal(err)
		}

		devicesDir := filepath.Join(tmpDir, ".openclaw", "devices")
		if err := os.MkdirAll(devicesDir, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(devicesDir, "paired.json"), []byte(`{
			"dev-2": {
				"deviceId": "dev-2",
				"scopes": ["operator.read"],
				"tokens": {"operator":{"role":"operator","scopes":["operator.read"]}}
			}
		}`), 0o600); err != nil {
			t.Fatal(err)
		}

		patchDeviceScopes()

		data, err := os.ReadFile(filepath.Join(devicesDir, "paired.json"))
		if err != nil {
			t.Fatal(err)
		}
		var devices map[string]map[string]any
		if err := json.Unmarshal(data, &devices); err != nil {
			t.Fatal(err)
		}
		dev := devices["dev-2"]
		if _, ok := dev["approvedScopes"]; !ok {
			t.Fatal("expected approvedScopes to be created")
		}
	})
}

func TestPrintOpenclawReady(t *testing.T) {
	t.Run("includes port in URL", func(t *testing.T) {
		var buf bytes.Buffer
		old := os.Stderr
		r, w, _ := os.Pipe()
		os.Stderr = w

		printOpenclawReady("openclaw", "", 9999, false)

		w.Close()
		os.Stderr = old
		buf.ReadFrom(r)

		output := buf.String()
		if !strings.Contains(output, "127.0.0.1:9999") {
			t.Errorf("expected port 9999 in output, got:\n%s", output)
		}
		if strings.Contains(output, "#token=") {
			t.Error("should not include token fragment when token is empty")
		}
	})

	t.Run("URL-escapes token", func(t *testing.T) {
		var buf bytes.Buffer
		old := os.Stderr
		r, w, _ := os.Pipe()
		os.Stderr = w

		printOpenclawReady("openclaw", "my token&special=chars", defaultGatewayPort, false)

		w.Close()
		os.Stderr = old
		buf.ReadFrom(r)

		output := buf.String()
		escaped := url.QueryEscape("my token&special=chars")
		if !strings.Contains(output, "#token="+escaped) {
			t.Errorf("expected URL-escaped token %q in output, got:\n%s", escaped, output)
		}
	})

	t.Run("simple token is not mangled", func(t *testing.T) {
		var buf bytes.Buffer
		old := os.Stderr
		r, w, _ := os.Pipe()
		os.Stderr = w

		printOpenclawReady("openclaw", "ollama", defaultGatewayPort, false)

		w.Close()
		os.Stderr = old
		buf.ReadFrom(r)

		output := buf.String()
		if !strings.Contains(output, "#token=ollama") {
			t.Errorf("expected #token=ollama in output, got:\n%s", output)
		}
	})

	t.Run("includes web UI hint", func(t *testing.T) {
		var buf bytes.Buffer
		old := os.Stderr
		r, w, _ := os.Pipe()
		os.Stderr = w

		printOpenclawReady("openclaw", "", defaultGatewayPort, false)

		w.Close()
		os.Stderr = old
		buf.ReadFrom(r)

		output := buf.String()
		if !strings.Contains(output, "Open the Web UI") {
			t.Errorf("expected web UI hint in output, got:\n%s", output)
		}
	})

	t.Run("first launch shows quick start tips", func(t *testing.T) {
		var buf bytes.Buffer
		old := os.Stderr
		r, w, _ := os.Pipe()
		os.Stderr = w

		printOpenclawReady("openclaw", "ollama", defaultGatewayPort, true)

		w.Close()
		os.Stderr = old
		buf.ReadFrom(r)

		output := buf.String()
		for _, want := range []string{"/help", "skills", "gateway"} {
			if !strings.Contains(output, want) {
				t.Errorf("expected %q in first-launch output, got:\n%s", want, output)
			}
		}
		if strings.Contains(output, "configure --section channels") {
			t.Errorf("did not expect channels configure tip in first-launch output, got:\n%s", output)
		}
	})

	t.Run("subsequent launch omits quick start tips", func(t *testing.T) {
		var buf bytes.Buffer
		old := os.Stderr
		r, w, _ := os.Pipe()
		os.Stderr = w

		printOpenclawReady("openclaw", "ollama", defaultGatewayPort, false)

		w.Close()
		os.Stderr = old
		buf.ReadFrom(r)

		output := buf.String()
		if strings.Contains(output, "Quick start") {
			t.Errorf("should not show quick start on subsequent launch")
		}
		if strings.Contains(output, "browse skills with") {
			t.Errorf("should not show repeated skills tip on subsequent launch")
		}
		if strings.Contains(output, "configure --section channels") {
			t.Errorf("did not expect channels configure tip on subsequent launch, got:\n%s", output)
		}
	})
}

func TestOpenclawModelConfig(t *testing.T) {
	t.Run("minimal model returns base config", func(t *testing.T) {
		cfg, _ := openclawModelConfig(fallbackLaunchModel("llama3.2"))

		if cfg["id"] != "llama3.2" {
			t.Errorf("id = %v, want llama3.2", cfg["id"])
		}
		if cfg["name"] != "llama3.2" {
			t.Errorf("name = %v, want llama3.2", cfg["name"])
		}
		if cfg["cost"] == nil {
			t.Error("cost should be set")
		}
		// Should not have capability fields without inventory metadata.
		if _, ok := cfg["reasoning"]; ok {
			t.Error("reasoning should not be set without metadata")
		}
		if _, ok := cfg["contextWindow"]; ok {
			t.Error("contextWindow should not be set without metadata")
		}
	})

	t.Run("sets vision input when model has vision capability", func(t *testing.T) {
		cfg, _ := openclawModelConfig(LaunchModel{Name: "llava:7b", Capabilities: []model.Capability{"vision"}, ContextLength: 4096})

		input, ok := cfg["input"].([]any)
		if !ok || len(input) != 2 {
			t.Errorf("input = %v, want [text image]", cfg["input"])
		}
	})

	t.Run("sets text-only input when model lacks vision", func(t *testing.T) {
		cfg, _ := openclawModelConfig(LaunchModel{Name: "llama3.2", Capabilities: []model.Capability{"completion"}})

		input, ok := cfg["input"].([]any)
		if !ok || len(input) != 1 {
			t.Errorf("input = %v, want [text]", cfg["input"])
		}
		if _, ok := cfg["reasoning"]; ok {
			t.Error("reasoning should not be set for non-thinking model")
		}
	})

	t.Run("sets reasoning when model has thinking capability", func(t *testing.T) {
		cfg, _ := openclawModelConfig(LaunchModel{Name: "qwq", Capabilities: []model.Capability{"thinking"}})

		if cfg["reasoning"] != true {
			t.Error("expected reasoning = true for thinking model")
		}
	})

	t.Run("sets context window from inventory metadata", func(t *testing.T) {
		cfg, _ := openclawModelConfig(LaunchModel{Name: "llama3.2", ContextLength: 131072})

		if cfg["contextWindow"] != 131072 {
			t.Errorf("contextWindow = %v, want 131072", cfg["contextWindow"])
		}
	})

	t.Run("handles all capabilities together", func(t *testing.T) {
		cfg, _ := openclawModelConfig(LaunchModel{
			Name:          "qwen3-vision",
			Capabilities:  []model.Capability{"vision", "thinking"},
			ContextLength: 32768,
		})

		input, ok := cfg["input"].([]any)
		if !ok || len(input) != 2 {
			t.Errorf("input = %v, want [text image]", cfg["input"])
		}
		if cfg["reasoning"] != true {
			t.Error("expected reasoning = true")
		}
		if cfg["contextWindow"] != 32768 {
			t.Errorf("contextWindow = %v, want 32768", cfg["contextWindow"])
		}
	})

	t.Run("returns base config when metadata is unavailable", func(t *testing.T) {
		cfg, _ := openclawModelConfig(fallbackLaunchModel("missing-model"))

		if cfg["id"] != "missing-model" {
			t.Errorf("id = %v, want missing-model", cfg["id"])
		}
		// Should still have input (default)
		if cfg["input"] == nil {
			t.Error("input should always be set")
		}
		if _, ok := cfg["reasoning"]; ok {
			t.Error("reasoning should not be set when metadata is unavailable")
		}
		if _, ok := cfg["contextWindow"]; ok {
			t.Error("contextWindow should not be set when metadata is unavailable")
		}
	})

	t.Run("skips zero context length", func(t *testing.T) {
		cfg, _ := openclawModelConfig(LaunchModel{Name: "test-model", ContextLength: 0})

		if _, ok := cfg["contextWindow"]; ok {
			t.Error("contextWindow should not be set for zero value")
		}
	})

	t.Run("cloud model uses hardcoded limits", func(t *testing.T) {
		cfg, isCloud := openclawModelConfig(fallbackLaunchModel("minimax-m2.7:cloud"))

		if !isCloud {
			t.Error("expected isCloud = true for cloud model")
		}
		if cfg["contextWindow"] != 204_800 {
			t.Errorf("contextWindow = %v, want 204800", cfg["contextWindow"])
		}
		if cfg["maxTokens"] != 128_000 {
			t.Errorf("maxTokens = %v, want 128000", cfg["maxTokens"])
		}
	})

	t.Run("cloud model with vision capability gets image input", func(t *testing.T) {
		cfg, isCloud := openclawModelConfig(LaunchModel{
			Name:         "qwen3-vl:235b-cloud",
			Remote:       true,
			Capabilities: []model.Capability{"vision"},
		}.WithCloudLimits())

		if !isCloud {
			t.Error("expected isCloud = true for cloud vision model")
		}
		input, ok := cfg["input"].([]any)
		if !ok || len(input) != 2 {
			t.Errorf("input = %v, want [text image] for cloud vision model", cfg["input"])
		}
	})

	t.Run("cloud model with thinking capability gets reasoning flag", func(t *testing.T) {
		cfg, isCloud := openclawModelConfig(LaunchModel{
			Name:         "qwq:cloud",
			Remote:       true,
			Capabilities: []model.Capability{"thinking"},
		})

		if !isCloud {
			t.Error("expected isCloud = true for cloud thinking model")
		}
		if cfg["reasoning"] != true {
			t.Error("expected reasoning = true for cloud thinking model")
		}
	})
}

func TestIntegrationOnboarded(t *testing.T) {
	t.Run("returns false when not set", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		integrationConfig, err := LoadIntegration("openclaw")
		if err == nil && integrationConfig.Onboarded {
			t.Error("expected false for fresh config")
		}
	})

	t.Run("returns true after integrationOnboarded", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		os.MkdirAll(filepath.Join(tmpDir, ".ollama"), 0o755)

		if err := integrationOnboarded("openclaw"); err != nil {
			t.Fatal(err)
		}
		integrationConfig, err := LoadIntegration("openclaw")
		if err != nil || !integrationConfig.Onboarded {
			t.Error("expected true after integrationOnboarded")
		}
	})

	t.Run("is case insensitive", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		os.MkdirAll(filepath.Join(tmpDir, ".ollama"), 0o755)

		if err := integrationOnboarded("OpenClaw"); err != nil {
			t.Fatal(err)
		}
		integrationConfig, err := LoadIntegration("openclaw")
		if err != nil || !integrationConfig.Onboarded {
			t.Error("expected true when set with different case")
		}
	})

	t.Run("preserves existing integration data", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		os.MkdirAll(filepath.Join(tmpDir, ".ollama"), 0o755)

		if err := SaveIntegration("openclaw", []string{"llama3.2", "mistral"}); err != nil {
			t.Fatal(err)
		}
		if err := integrationOnboarded("openclaw"); err != nil {
			t.Fatal(err)
		}

		// Verify onboarded is set
		integrationConfig, err := LoadIntegration("openclaw")
		if err != nil || !integrationConfig.Onboarded {
			t.Error("expected true after integrationOnboarded")
		}

		// Verify models are preserved
		model := IntegrationModel("openclaw")
		if model != "llama3.2" {
			t.Errorf("expected first model llama3.2, got %q", model)
		}
	})
}

func TestConfigureOllamaWebSearch(t *testing.T) {
	home := t.TempDir()
	setTestHome(t, home)

	configDir := filepath.Join(home, ".openclaw")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatal(err)
	}
	configPath := filepath.Join(configDir, "openclaw.json")

	t.Run("fresh config", func(t *testing.T) {
		if err := os.WriteFile(configPath, []byte(`{}`), 0o644); err != nil {
			t.Fatal(err)
		}

		configureOllamaWebSearch()

		data, err := os.ReadFile(configPath)
		if err != nil {
			t.Fatal(err)
		}
		var config map[string]any
		if err := json.Unmarshal(data, &config); err != nil {
			t.Fatal(err)
		}

		plugins, _ := config["plugins"].(map[string]any)
		if plugins == nil {
			t.Fatal("plugins section missing")
		}

		entries, _ := plugins["entries"].(map[string]any)
		entry, _ := entries["ollama"].(map[string]any)
		if enabled, _ := entry["enabled"].(bool); !enabled {
			t.Error("expected entries.ollama.enabled = true")
		}
		if _, ok := entries["openclaw-web-search"]; ok {
			t.Error("expected stale openclaw-web-search entry to be absent")
		}

		if _, ok := plugins["allow"]; ok {
			t.Error("did not expect plugins.allow to be created when no allowlist exists")
		}
		if _, ok := plugins["installs"]; ok {
			t.Error("did not expect plugins.installs to be created")
		}

		tools, _ := config["tools"].(map[string]any)
		web, _ := tools["web"].(map[string]any)
		search, _ := web["search"].(map[string]any)
		if got, _ := search["provider"].(string); got != "ollama" {
			t.Errorf("search provider = %q, want %q", got, "ollama")
		}
		if enabled, _ := search["enabled"].(bool); !enabled {
			t.Error("expected tools.web.search.enabled = true")
		}
	})

	t.Run("idempotent", func(t *testing.T) {
		if err := os.WriteFile(configPath, []byte(`{}`), 0o644); err != nil {
			t.Fatal(err)
		}

		configureOllamaWebSearch()
		configureOllamaWebSearch()

		data, err := os.ReadFile(configPath)
		if err != nil {
			t.Fatal(err)
		}
		var config map[string]any
		if err := json.Unmarshal(data, &config); err != nil {
			t.Fatal(err)
		}

		plugins, _ := config["plugins"].(map[string]any)
		entries, _ := plugins["entries"].(map[string]any)
		if len(entries) != 1 {
			t.Fatalf("expected only bundled ollama entry, got %v", entries)
		}
		if _, ok := entries["ollama"]; !ok {
			t.Fatalf("expected entries.ollama to exist, got %v", entries)
		}
	})

	t.Run("migrates stale plugin config and preserves unrelated settings", func(t *testing.T) {
		initial := map[string]any{
			"plugins": map[string]any{
				"allow": []any{"some-other-plugin", "openclaw-web-search"},
				"entries": map[string]any{
					"some-other-plugin":   map[string]any{"enabled": true},
					"openclaw-web-search": map[string]any{"enabled": true},
				},
				"installs": map[string]any{
					"some-other-plugin": map[string]any{
						"source":      "npm",
						"installPath": "/some/path",
					},
					"openclaw-web-search": map[string]any{
						"source":      "npm",
						"installPath": "/old/path",
					},
				},
			},
			"tools": map[string]any{
				"alsoAllow": []any{"ollama_web_search", "ollama_web_fetch", "browser"},
				"web": map[string]any{
					"search": map[string]any{"enabled": false},
					"fetch":  map[string]any{"enabled": false},
				},
			},
			"customField": "preserved",
		}
		data, _ := json.Marshal(initial)
		if err := os.WriteFile(configPath, data, 0o644); err != nil {
			t.Fatal(err)
		}

		configureOllamaWebSearch()

		out, err := os.ReadFile(configPath)
		if err != nil {
			t.Fatal(err)
		}
		var config map[string]any
		if err := json.Unmarshal(out, &config); err != nil {
			t.Fatal(err)
		}

		if config["customField"] != "preserved" {
			t.Error("customField was not preserved")
		}

		plugins, _ := config["plugins"].(map[string]any)
		entries, _ := plugins["entries"].(map[string]any)
		if entries["some-other-plugin"] == nil {
			t.Error("existing plugin entry was lost")
		}
		if entries["openclaw-web-search"] != nil {
			t.Error("stale openclaw-web-search entry should be removed")
		}
		if ollamaEntry, _ := entries["ollama"].(map[string]any); ollamaEntry == nil {
			t.Fatal("expected bundled ollama entry to be enabled")
		}

		installs, _ := plugins["installs"].(map[string]any)
		if installs["some-other-plugin"] == nil {
			t.Error("existing install record was lost")
		}
		if installs["openclaw-web-search"] != nil {
			t.Error("stale openclaw-web-search install record should be removed")
		}

		allow, _ := plugins["allow"].([]any)
		hasOther, hasStalePlugin, hasOllama := false, false, false
		for _, v := range allow {
			s, _ := v.(string)
			if s == "some-other-plugin" {
				hasOther = true
			}
			if s == "openclaw-web-search" {
				hasStalePlugin = true
			}
			if s == "ollama" {
				hasOllama = true
			}
		}
		if !hasOther {
			t.Error("existing allow entry was lost")
		}
		if hasStalePlugin {
			t.Error("stale openclaw-web-search allow entry should be removed")
		}
		if !hasOllama {
			t.Error("expected plugins.allow to contain bundled ollama plugin")
		}

		tools, _ := config["tools"].(map[string]any)
		alsoAllow, _ := tools["alsoAllow"].([]any)
		if len(alsoAllow) != 1 || alsoAllow[0] != "browser" {
			t.Errorf("expected stale custom web tools to be removed, got %v", alsoAllow)
		}
		web, _ := tools["web"].(map[string]any)
		search, _ := web["search"].(map[string]any)
		fetch, _ := web["fetch"].(map[string]any)
		if got, _ := search["provider"].(string); got != "ollama" {
			t.Errorf("search provider = %q, want %q", got, "ollama")
		}
		if enabled, _ := search["enabled"].(bool); !enabled {
			t.Error("expected migrated tools.web.search.enabled = true")
		}
		if enabled, _ := fetch["enabled"].(bool); !enabled {
			t.Error("expected migrated tools.web.fetch.enabled = true")
		}
	})
}

func TestClearSessionModelOverride(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	sessionsDir := filepath.Join(tmpDir, ".openclaw", "agents", "main", "sessions")
	sessionsPath := filepath.Join(sessionsDir, "sessions.json")

	writeSessionsFile := func(t *testing.T, sessions map[string]map[string]any) {
		t.Helper()
		if err := os.MkdirAll(sessionsDir, 0o755); err != nil {
			t.Fatal(err)
		}
		data, err := json.Marshal(sessions)
		if err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(sessionsPath, data, 0o600); err != nil {
			t.Fatal(err)
		}
	}

	readSessionsFile := func(t *testing.T) map[string]map[string]any {
		t.Helper()
		data, err := os.ReadFile(sessionsPath)
		if err != nil {
			t.Fatalf("reading sessions file: %v", err)
		}
		var sessions map[string]map[string]any
		if err := json.Unmarshal(data, &sessions); err != nil {
			t.Fatalf("parsing sessions file: %v", err)
		}
		return sessions
	}

	t.Run("clears modelOverride and updates model", func(t *testing.T) {
		writeSessionsFile(t, map[string]map[string]any{
			"sess1": {"model": "ollama/old-model", "modelOverride": "old-model", "providerOverride": "ollama"},
		})
		clearSessionModelOverride("new-model")
		sessions := readSessionsFile(t)
		sess := sessions["sess1"]
		if _, ok := sess["modelOverride"]; ok {
			t.Error("modelOverride should have been deleted")
		}
		if _, ok := sess["providerOverride"]; ok {
			t.Error("providerOverride should have been deleted")
		}
		if sess["model"] != "new-model" {
			t.Errorf("model = %q, want %q", sess["model"], "new-model")
		}
	})

	t.Run("updates model field in sessions without modelOverride", func(t *testing.T) {
		// This is the bug case: session has model pointing to old primary,
		// but no explicit modelOverride. After changing primary, the session
		// model field must also be updated.
		writeSessionsFile(t, map[string]map[string]any{
			"sess1": {"model": "ollama/old-model"},
		})
		clearSessionModelOverride("new-model")
		sessions := readSessionsFile(t)
		if sessions["sess1"]["model"] != "new-model" {
			t.Errorf("model = %q, want %q", sessions["sess1"]["model"], "new-model")
		}
	})

	t.Run("does not update session already using primary", func(t *testing.T) {
		writeSessionsFile(t, map[string]map[string]any{
			"sess1": {"model": "current-model"},
		})
		clearSessionModelOverride("current-model")
		sessions := readSessionsFile(t)
		if sessions["sess1"]["model"] != "current-model" {
			t.Errorf("model = %q, want %q", sessions["sess1"]["model"], "current-model")
		}
	})

	t.Run("does not update session with empty model field", func(t *testing.T) {
		writeSessionsFile(t, map[string]map[string]any{
			"sess1": {"other": "data"},
		})
		clearSessionModelOverride("new-model")
		sessions := readSessionsFile(t)
		if _, ok := sessions["sess1"]["model"]; ok {
			t.Error("model field should not have been added to session with no model")
		}
	})

	t.Run("handles multiple sessions mixed", func(t *testing.T) {
		writeSessionsFile(t, map[string]map[string]any{
			"with-override":    {"model": "old", "modelOverride": "old", "providerOverride": "ollama"},
			"without-override": {"model": "old"},
			"already-current":  {"model": "new-model"},
			"no-model":         {"other": "data"},
		})
		clearSessionModelOverride("new-model")
		sessions := readSessionsFile(t)

		if sessions["with-override"]["model"] != "new-model" {
			t.Errorf("with-override model = %q, want %q", sessions["with-override"]["model"], "new-model")
		}
		if _, ok := sessions["with-override"]["modelOverride"]; ok {
			t.Error("with-override: modelOverride should be deleted")
		}
		if sessions["without-override"]["model"] != "new-model" {
			t.Errorf("without-override model = %q, want %q", sessions["without-override"]["model"], "new-model")
		}
		if sessions["already-current"]["model"] != "new-model" {
			t.Errorf("already-current model = %q, want %q", sessions["already-current"]["model"], "new-model")
		}
		if _, ok := sessions["no-model"]["model"]; ok {
			t.Error("no-model: model should not have been added")
		}
	})

	t.Run("no-op when sessions file missing", func(t *testing.T) {
		os.RemoveAll(sessionsDir)
		clearSessionModelOverride("new-model") // should not panic or error
	})
}
