package opencode

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/chenhg5/cc-connect/core"
)

type errWriter struct{}

func (errWriter) Write(_ []byte) (int, error) {
	return 0, errors.New("write failed")
}

// writeFakeModelsBin writes a temporary shell script that acts as a fake CLI.
// When invoked with "models", it prints lines to stdout.
// When exitCode != 0, the script exits immediately with that code.
func writeFakeModelsBin(t *testing.T, lines []string, exitCode int) string {
	t.Helper()
	tmpDir := t.TempDir()
	name := filepath.Join(tmpDir, "fake-opencode")

	var body strings.Builder
	body.WriteString("#!/bin/sh\n")
	if exitCode != 0 {
		fmt.Fprintf(&body, "exit %d\n", exitCode)
	} else {
		body.WriteString("if [ \"$1\" = \"models\" ]; then\n")
		for _, line := range lines {
			fmt.Fprintf(&body, "printf '%%s\\n' '%s'\n", line)
		}
		body.WriteString("fi\n")
	}

	if err := os.WriteFile(name, []byte(body.String()), 0755); err != nil {
		t.Fatal(err)
	}
	return name
}

func TestWriteProviderSignaturePart_PropagatesWriterError(t *testing.T) {
	err := writeProviderSignaturePart(errWriter{}, "name", "value")
	if err == nil {
		t.Fatal("writeProviderSignaturePart() error = nil, want write failure")
	}
	if !strings.Contains(err.Error(), "write failed") {
		t.Fatalf("writeProviderSignaturePart() error = %v, want write failure", err)
	}
}

func writePersistentModelCache(t *testing.T, cachePath string, models []core.ModelOption, updatedAt time.Time) string {
	t.Helper()

	type persistentModelCache struct {
		Models     []core.ModelOption `json:"models"`
		UpdatedAt  time.Time          `json:"updated_at"`
		ContextKey string             `json:"context_key,omitempty"`
	}

	if err := os.MkdirAll(filepath.Dir(cachePath), 0o755); err != nil {
		t.Fatal(err)
	}

	defaultSnapshot := opencodeModelDiscoverySnapshot{workDir: "."}
	data, err := json.Marshal(persistentModelCache{Models: models, UpdatedAt: updatedAt, ContextKey: modelDiscoveryContextKey(defaultSnapshot)})
	if err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(cachePath, data, 0o644); err != nil {
		t.Fatal(err)
	}
	return cachePath
}

func writePersistentModelCacheWithSnapshot(t *testing.T, cachePath string, snapshot opencodeModelDiscoverySnapshot, models []core.ModelOption, updatedAt time.Time) string {
	t.Helper()

	type persistentModelCache struct {
		Models      []core.ModelOption `json:"models"`
		UpdatedAt   time.Time          `json:"updated_at"`
		ProviderKey string             `json:"provider_key,omitempty"`
		ContextKey  string             `json:"context_key,omitempty"`
	}

	if err := os.MkdirAll(filepath.Dir(cachePath), 0o755); err != nil {
		t.Fatal(err)
	}

	data, err := json.Marshal(persistentModelCache{
		Models:      models,
		UpdatedAt:   updatedAt,
		ProviderKey: snapshot.providerKey,
		ContextKey:  modelDiscoveryContextKey(snapshot),
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(cachePath, data, 0o644); err != nil {
		t.Fatal(err)
	}
	return cachePath
}

func writeBlockingModelsBin(t *testing.T, gatePath string, lines []string) string {
	t.Helper()
	tmpDir := t.TempDir()
	name := filepath.Join(tmpDir, "fake-opencode")

	var body strings.Builder
	body.WriteString("#!/bin/sh\n")
	body.WriteString("if [ \"$1\" = \"models\" ]; then\n")
	if gatePath != "" {
		fmt.Fprintf(&body, "  while [ ! -f '%s' ]; do\n", gatePath)
		body.WriteString("    sleep 0.01\n")
		body.WriteString("  done\n")
	}
	for _, line := range lines {
		fmt.Fprintf(&body, "  printf '%%s\\n' '%s'\n", line)
	}
	body.WriteString("fi\n")

	if err := os.WriteFile(name, []byte(body.String()), 0o755); err != nil {
		t.Fatal(err)
	}
	return name
}

func writeCountingModelsBin(t *testing.T, countPath, gatePath string, lines []string, requireEnvKey string, exitCode int) string {
	t.Helper()
	tmpDir := t.TempDir()
	name := filepath.Join(tmpDir, "fake-opencode")

	var body strings.Builder
	body.WriteString("#!/bin/sh\n")
	body.WriteString("if [ \"$1\" = \"models\" ]; then\n")
	if countPath != "" {
		fmt.Fprintf(&body, "  count=0\n  if [ -f '%s' ]; then count=$(cat '%s'); fi\n", countPath, countPath)
		fmt.Fprintf(&body, "  count=$((count + 1))\n  printf '%%s' \"$count\" > '%s'\n", countPath)
	}
	if gatePath != "" {
		fmt.Fprintf(&body, "  while [ ! -f '%s' ]; do\n", gatePath)
		body.WriteString("    sleep 0.01\n")
		body.WriteString("  done\n")
	}
	if requireEnvKey != "" {
		fmt.Fprintf(&body, "  if [ -z \"$%s\" ]; then exit 0; fi\n", requireEnvKey)
	}
	if exitCode != 0 {
		fmt.Fprintf(&body, "  exit %d\n", exitCode)
	} else {
		for _, line := range lines {
			fmt.Fprintf(&body, "  printf '%%s\\n' '%s'\n", line)
		}
	}
	body.WriteString("fi\n")

	if err := os.WriteFile(name, []byte(body.String()), 0o755); err != nil {
		t.Fatal(err)
	}
	return name
}

func waitForModelsInPersistentCache(t *testing.T, cachePath string, want []string) {
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		cache, err := loadOpencodePersistentModelCache(cachePath)
		if err == nil && cache != nil && len(cache.Models) == len(want) {
			match := true
			for i, model := range cache.Models {
				if model.Name != want[i] {
					match = false
					break
				}
			}
			if match {
				return
			}
		}
		time.Sleep(10 * time.Millisecond)
	}

	cache, err := loadOpencodePersistentModelCache(cachePath)
	if err != nil {
		t.Fatalf("loadOpencodePersistentModelCache(%q) error = %v", cachePath, err)
	}
	t.Fatalf("persistent cache models = %v, want %v", cache, want)
}

func waitForFileContent(t *testing.T, path, want string) {
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		data, err := os.ReadFile(path)
		if err == nil && strings.TrimSpace(string(data)) == want {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("os.ReadFile(%q) error = %v", path, err)
	}
	t.Fatalf("file %q content = %q, want %q", path, strings.TrimSpace(string(data)), want)
}

func readPersistentModelCachePayload(t *testing.T, cachePath string) map[string]any {
	t.Helper()
	data, err := os.ReadFile(cachePath)
	if err != nil {
		t.Fatalf("os.ReadFile(%q) error = %v", cachePath, err)
	}
	var payload map[string]any
	if err := json.Unmarshal(data, &payload); err != nil {
		t.Fatalf("json.Unmarshal(%q) error = %v", cachePath, err)
	}
	return payload
}

func providerCacheKeyOf(t *testing.T, a *Agent) string {
	t.Helper()
	key := a.activeProviderKey()
	if key == "" {
		t.Fatal("activeProviderKey() = empty, want signature")
	}
	if strings.Contains(key, "provider-") || strings.Contains(key, "present") || strings.Contains(key, "secret") {
		t.Fatalf("activeProviderKey() leaked raw provider data: %q", key)
	}
	return key
}

func TestConfiguredModels_BoundaryConditions(t *testing.T) {
	a := &Agent{
		providers: []core.ProviderConfig{
			{Models: []core.ModelOption{{Name: "first"}}},
			{Models: []core.ModelOption{{Name: "second"}}},
		},
	}

	tests := []struct {
		name      string
		activeIdx int
		wantNil   bool
		wantName  string
	}{
		{name: "negative index", activeIdx: -1, wantNil: true},
		{name: "out of range", activeIdx: 2, wantNil: true},
		{name: "valid index", activeIdx: 1, wantName: "second"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a.activeIdx = tt.activeIdx
			got := a.configuredModels()
			if tt.wantNil {
				if got != nil {
					t.Fatalf("configuredModels() = %v, want nil", got)
				}
				return
			}
			if len(got) != 1 || got[0].Name != tt.wantName {
				t.Fatalf("configuredModels() = %v, want %q", got, tt.wantName)
			}
		})
	}
}

func TestNormalizeMode(t *testing.T) {
	tests := []struct {
		input    string
		expected string
	}{
		{"yolo", "yolo"},
		{"YOLO", "yolo"},
		{"auto", "yolo"},
		{"AUTO", "yolo"},
		{"force", "yolo"},
		{"bypasspermissions", "yolo"},
		{"default", "default"},
		{"DEFAULT", "default"},
		{"", "default"},
		{"unknown", "default"},
		{"  yolo  ", "yolo"},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			got := normalizeMode(tt.input)
			if got != tt.expected {
				t.Errorf("normalizeMode(%q) = %q, want %q", tt.input, got, tt.expected)
			}
		})
	}
}

func TestAgent_Name(t *testing.T) {
	a := &Agent{}
	if got := a.Name(); got != "opencode" {
		t.Errorf("Name() = %q, want %q", got, "opencode")
	}
}

func TestAgent_SetModel(t *testing.T) {
	a := &Agent{}
	a.SetModel("gpt-4")
	if got := a.GetModel(); got != "gpt-4" {
		t.Errorf("GetModel() = %q, want %q", got, "gpt-4")
	}
}

func TestAgent_SetMode(t *testing.T) {
	a := &Agent{}
	a.SetMode("yolo")
	if got := a.GetMode(); got != "yolo" {
		t.Errorf("GetMode() = %q, want %q", got, "yolo")
	}
}

func TestAgent_GetActiveProvider(t *testing.T) {
	a := &Agent{
		providers: []core.ProviderConfig{
			{Name: "openai"},
			{Name: "anthropic"},
		},
		activeIdx: 1,
	}
	got := a.GetActiveProvider()
	if got == nil {
		t.Fatal("GetActiveProvider() returned nil")
	}
	if got.Name != "anthropic" {
		t.Errorf("GetActiveProvider().Name = %q, want %q", got.Name, "anthropic")
	}
}

func TestAgent_GetActiveProvider_NoActive(t *testing.T) {
	a := &Agent{
		providers: []core.ProviderConfig{
			{Name: "openai"},
		},
		activeIdx: -1,
	}
	if got := a.GetActiveProvider(); got != nil {
		t.Errorf("GetActiveProvider() = %v, want nil", got)
	}
}

func TestAgent_ListProviders(t *testing.T) {
	providers := []core.ProviderConfig{
		{Name: "openai"},
		{Name: "anthropic"},
	}
	a := &Agent{providers: providers}
	got := a.ListProviders()
	if len(got) != 2 {
		t.Errorf("ListProviders() returned %d providers, want 2", len(got))
	}
}

func TestAgent_SetActiveProvider(t *testing.T) {
	a := &Agent{
		providers: []core.ProviderConfig{
			{Name: "openai"},
			{Name: "anthropic"},
		},
	}
	if !a.SetActiveProvider("anthropic") {
		t.Error("SetActiveProvider(\"anthropic\") returned false")
	}
	if got := a.GetActiveProvider(); got == nil || got.Name != "anthropic" {
		t.Errorf("GetActiveProvider().Name = %q, want %q", got.Name, "anthropic")
	}
}

func TestAgent_SetActiveProvider_Invalid(t *testing.T) {
	a := &Agent{
		providers: []core.ProviderConfig{
			{Name: "openai"},
		},
	}
	if a.SetActiveProvider("nonexistent") {
		t.Error("SetActiveProvider(\"nonexistent\") returned true, want false")
	}
}

// ---------- dynamic discovery tests ----------

// TestAvailableModels_UsesDynamicDiscovery verifies that AvailableModels returns
// the model list produced by `opencode models` when it succeeds.
func TestAvailableModels_UsesDynamicDiscovery(t *testing.T) {
	bin := writeFakeModelsBin(t, []string{"anthropic/claude-3-5-sonnet", "openai/gpt-4o"}, 0)
	a := &Agent{cmd: bin, activeIdx: -1}

	got := a.AvailableModels(context.Background())
	if len(got) != 2 {
		t.Fatalf("AvailableModels() = %v (len %d), want 2 models", got, len(got))
	}
	// results must be sorted
	if got[0].Name != "anthropic/claude-3-5-sonnet" {
		t.Errorf("got[0].Name = %q, want %q", got[0].Name, "anthropic/claude-3-5-sonnet")
	}
	if got[1].Name != "openai/gpt-4o" {
		t.Errorf("got[1].Name = %q, want %q", got[1].Name, "openai/gpt-4o")
	}
}

// TestAvailableModels_DynamicTakesPriorityOverConfigured verifies discovery beats
// provider-configured models.
func TestAvailableModels_DynamicTakesPriorityOverConfigured(t *testing.T) {
	bin := writeFakeModelsBin(t, []string{"discovered/model"}, 0)
	a := &Agent{
		cmd: bin,
		providers: []core.ProviderConfig{
			{Models: []core.ModelOption{{Name: "configured/model"}}},
		},
		activeIdx: 0,
	}

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "discovered/model" {
		t.Errorf("AvailableModels() = %v, want [discovered/model]", got)
	}
}

// TestAvailableModels_FallsBackToConfiguredOnDiscoveryFail verifies fallback to
// provider-configured models when `opencode models` exits non-zero.
func TestAvailableModels_FallsBackToConfiguredOnDiscoveryFail(t *testing.T) {
	bin := writeFakeModelsBin(t, nil, 1) // exits with error
	a := &Agent{
		cmd: bin,
		providers: []core.ProviderConfig{
			{Models: []core.ModelOption{{Name: "configured-model"}}},
		},
		activeIdx: 0,
	}

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "configured-model" {
		t.Errorf("AvailableModels() = %v, want [configured-model]", got)
	}
}

// TestAvailableModels_FallsBackToBuiltinWhenBothUnavailable verifies the final
// fallback to the hardcoded built-in model list.
func TestAvailableModels_FallsBackToBuiltinWhenBothUnavailable(t *testing.T) {
	bin := writeFakeModelsBin(t, nil, 1)
	a := &Agent{cmd: bin, activeIdx: -1}

	got := a.AvailableModels(context.Background())
	if len(got) == 0 {
		t.Fatal("AvailableModels() returned empty list, want built-in fallback")
	}
	found := false
	for _, m := range got {
		if m.Name == "anthropic/claude-sonnet-4-20250514" {
			found = true
			break
		}
	}
	if !found {
		t.Errorf("AvailableModels() built-in fallback missing expected model; got: %v", got)
	}
}

// TestAvailableModels_DeduplicatesDiscoveredModels verifies that duplicate model
// names from the CLI output appear only once.
func TestAvailableModels_DeduplicatesDiscoveredModels(t *testing.T) {
	bin := writeFakeModelsBin(t, []string{"openai/gpt-4o", "openai/gpt-4o", "anthropic/claude"}, 0)
	a := &Agent{cmd: bin, activeIdx: -1}

	got := a.AvailableModels(context.Background())
	if len(got) != 2 {
		t.Fatalf("AvailableModels() = %v (len %d), want 2 after dedup", got, len(got))
	}
}

// TestAvailableModels_SortsDiscoveredModels verifies lexicographic sort order.
func TestAvailableModels_SortsDiscoveredModels(t *testing.T) {
	bin := writeFakeModelsBin(t, []string{"z-model", "a-model", "m-model"}, 0)
	a := &Agent{cmd: bin, activeIdx: -1}

	got := a.AvailableModels(context.Background())
	if len(got) != 3 {
		t.Fatalf("AvailableModels() = %v, want 3 models", got)
	}
	names := make([]string, len(got))
	for i, m := range got {
		names[i] = m.Name
	}
	sorted := append([]string(nil), names...)
	sort.Strings(sorted)
	for i := range names {
		if names[i] != sorted[i] {
			t.Errorf("AvailableModels() not sorted: got %v", names)
			break
		}
	}
}

// TestAvailableModels_EmptyDiscoveryOutputFallsBackToConfigured verifies that an
// exit-0 but empty-output binary still triggers the fallback chain.
func TestAvailableModels_EmptyDiscoveryOutputFallsBackToConfigured(t *testing.T) {
	bin := writeFakeModelsBin(t, []string{}, 0) // exits 0 but no output
	a := &Agent{
		cmd: bin,
		providers: []core.ProviderConfig{
			{Models: []core.ModelOption{{Name: "fallback-model"}}},
		},
		activeIdx: 0,
	}

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "fallback-model" {
		t.Errorf("AvailableModels() empty discovery = %v, want [fallback-model]", got)
	}
}

func TestAvailableModels_ConfiguredFallbackUsesSnapshot(t *testing.T) {
	a := &Agent{
		providers: []core.ProviderConfig{
			{Name: "provider-a", Models: []core.ModelOption{{Name: "configured-a"}}},
			{Name: "provider-b", Models: []core.ModelOption{{Name: "configured-b"}}},
		},
		activeIdx: 0,
	}

	snapshot := a.modelDiscoverySnapshot()
	if !a.SetActiveProvider("provider-b") {
		t.Fatal("SetActiveProvider(provider-b) = false, want true")
	}

	got := a.configuredModelsForSnapshot(snapshot)
	if len(got) != 1 || got[0].Name != "configured-a" {
		t.Fatalf("configuredModelsForSnapshot() = %v, want [configured-a]", got)
	}
}

// TestAvailableModels_CustomCmdUsedForDiscovery verifies that a.cmd (not the
// literal string "opencode") is used when running the models sub-command.
func TestAvailableModels_CustomCmdUsedForDiscovery(t *testing.T) {
	tmpDir := t.TempDir()
	customBin := filepath.Join(tmpDir, "my-ai-cli")
	script := "#!/bin/sh\nif [ \"$1\" = \"models\" ]; then\nprintf '%s\\n' 'custom/model-a'\nfi\n"
	if err := os.WriteFile(customBin, []byte(script), 0755); err != nil {
		t.Fatal(err)
	}

	a := &Agent{cmd: customBin, activeIdx: -1}
	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "custom/model-a" {
		t.Errorf("AvailableModels() with custom cmd = %v, want [custom/model-a]", got)
	}
}

func TestProjectModelCachePath_SanitizesProjectName(t *testing.T) {
	got := opencodeProjectModelCachePath("/tmp/data", " ../team/demo project:alpha?beta ")
	if filepath.Dir(got) != filepath.Join("/tmp/data", "projects") {
		t.Fatalf("opencodeProjectModelCachePath() dir = %q, want %q", filepath.Dir(got), filepath.Join("/tmp/data", "projects"))
	}
	base := filepath.Base(got)
	if !strings.HasPrefix(base, "team-demo-project-alpha-beta-") {
		t.Fatalf("opencodeProjectModelCachePath() base = %q, want sanitized prefix", base)
	}
	if !strings.HasSuffix(base, ".opencode-models.json") {
		t.Fatalf("opencodeProjectModelCachePath() base = %q, want .opencode-models.json suffix", base)
	}
	hashSuffix := strings.TrimSuffix(strings.TrimPrefix(base, "team-demo-project-alpha-beta-"), ".opencode-models.json")
	if len(hashSuffix) != 16 {
		t.Fatalf("opencodeProjectModelCachePath() hash suffix = %q, want 16 hex chars", hashSuffix)
	}
}

func TestProjectModelCachePath_DistinguishesSanitizeCollisions(t *testing.T) {
	pathA := opencodeProjectModelCachePath("/tmp/data", "team/demo")
	pathB := opencodeProjectModelCachePath("/tmp/data", "team-demo")
	if pathA == pathB {
		t.Fatalf("opencodeProjectModelCachePath() collision: %q == %q", pathA, pathB)
	}
}

func TestLoadPersistentModelCache_NormalizesModels(t *testing.T) {
	cachePath := opencodeProjectModelCachePath(t.TempDir(), "demo")
	writePersistentModelCache(t, cachePath, []core.ModelOption{
		{Name: "  z-model  ", Desc: "z"},
		{Name: ""},
		{Name: "   "},
		{Name: "a-model", Desc: "first"},
		{Name: "a-model", Desc: "duplicate"},
		{Name: "  m-model", Desc: "m"},
	}, time.Now())

	cache, err := loadOpencodePersistentModelCache(cachePath)
	if err != nil {
		t.Fatalf("loadOpencodePersistentModelCache() error = %v", err)
	}
	if cache == nil {
		t.Fatal("loadOpencodePersistentModelCache() = nil, want cache")
	}

	got := cache.Models
	if len(got) != 3 {
		t.Fatalf("normalized cache models len = %d, want 3: %v", len(got), got)
	}
	want := []string{"a-model", "m-model", "z-model"}
	for i, model := range got {
		if model.Name != want[i] {
			t.Fatalf("normalized cache models[%d] = %q, want %q (all: %v)", i, model.Name, want[i], got)
		}
	}
	if got[0].Desc != "first" {
		t.Fatalf("normalized cache kept desc %q, want %q", got[0].Desc, "first")
	}
}

func TestNew_SurfacesPersistentModelCacheViaAvailableModels(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	writePersistentModelCache(t, cachePath, []core.ModelOption{{Name: "cached/model"}}, time.Now())
	fakeEmptyBin := writeFakeModelsBin(t, []string{}, 0)

	agent, err := New(map[string]any{
		"cmd":         fakeEmptyBin,
		"cc_data_dir": dataDir,
		"cc_project":  "demo",
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	switcher, ok := agent.(core.ModelSwitcher)
	if !ok {
		t.Fatalf("New() agent does not implement core.ModelSwitcher")
	}

	got := switcher.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "cached/model" {
		t.Fatalf("AvailableModels() = %v, want [cached/model]", got)
	}
}

func TestAvailableModels_PrefersPersistentCacheOverDiscoveredModels(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	writePersistentModelCache(t, cachePath, []core.ModelOption{{Name: "cached/model"}}, time.Now())
	bin := writeFakeModelsBin(t, []string{"fresh/model"}, 0)

	agent, err := New(map[string]any{
		"cmd":         bin,
		"cc_data_dir": dataDir,
		"cc_project":  "demo",
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	a := agent.(*Agent)
	// AvailableModels with a cached result launches a background refresh goroutine that
	// writes into dataDir. Register cleanup BEFORE calling AvailableModels so that
	// t.Cleanup runs in LIFO order: our Wait() fires first, then t.TempDir's RemoveAll.
	// Without this, the goroutine can still be writing when RemoveAll runs, causing
	// "TempDir RemoveAll cleanup: directory not empty" under -cover (see #1118).
	t.Cleanup(func() { a.refreshWg.Wait() })

	switcher, ok := agent.(core.ModelSwitcher)
	if !ok {
		t.Fatalf("New() agent does not implement core.ModelSwitcher")
	}

	got := switcher.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "cached/model" {
		t.Fatalf("AvailableModels() = %v, want [cached/model]", got)
	}
}

func TestAvailableModels_ReturnsPersistentCacheWhenDiscoveryFails(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	writePersistentModelCache(t, cachePath, []core.ModelOption{{Name: "cached/model"}}, time.Now())
	failingBin := writeFakeModelsBin(t, nil, 1)

	agent, err := New(map[string]any{
		"cmd":         failingBin,
		"cc_data_dir": dataDir,
		"cc_project":  "demo",
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	switcher, ok := agent.(core.ModelSwitcher)
	if !ok {
		t.Fatalf("New() agent does not implement core.ModelSwitcher")
	}

	got := switcher.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "cached/model" {
		t.Fatalf("AvailableModels() = %v, want [cached/model]", got)
	}
}

func TestAvailableModels_PersistsDiscoveryOnColdStart(t *testing.T) {
	dataDir := t.TempDir()
	bin := writeFakeModelsBin(t, []string{"fresh/model", "second/model"}, 0)

	agent, err := New(map[string]any{
		"cmd":         bin,
		"cc_data_dir": dataDir,
		"cc_project":  "demo",
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	a := agent.(*Agent)

	got := a.AvailableModels(context.Background())
	if len(got) != 2 || got[0].Name != "fresh/model" || got[1].Name != "second/model" {
		t.Fatalf("AvailableModels() = %v, want discovered models", got)
	}

	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	cache, err := loadOpencodePersistentModelCache(cachePath)
	if err != nil {
		t.Fatalf("loadOpencodePersistentModelCache(%q) error = %v", cachePath, err)
	}
	if cache == nil {
		t.Fatalf("loadOpencodePersistentModelCache(%q) = nil, want persisted cache", cachePath)
	}
	if len(cache.Models) != 2 || cache.Models[0].Name != "fresh/model" || cache.Models[1].Name != "second/model" {
		t.Fatalf("persisted cache models = %v, want discovered models", cache.Models)
	}

	inMemory := a.persistentModels()
	if len(inMemory) != 2 || inMemory[0].Name != "fresh/model" || inMemory[1].Name != "second/model" {
		t.Fatalf("in-memory persistent models = %v, want discovered models", inMemory)
	}
}

func TestAvailableModels_BackgroundRefreshUpdatesDiskCache(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	writePersistentModelCache(t, cachePath, []core.ModelOption{{Name: "cached/model"}}, time.Now())
	gatePath := filepath.Join(t.TempDir(), "refresh-ready")
	bin := writeBlockingModelsBin(t, gatePath, []string{"fresh/model", "second/model"})

	agent, err := New(map[string]any{
		"cmd":         bin,
		"cc_data_dir": dataDir,
		"cc_project":  "demo",
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	switcher, ok := agent.(core.ModelSwitcher)
	if !ok {
		t.Fatalf("New() agent does not implement core.ModelSwitcher")
	}

	got := switcher.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "cached/model" {
		t.Fatalf("AvailableModels() = %v, want immediate cached result", got)
	}

	if err := os.WriteFile(gatePath, []byte("ok"), 0o644); err != nil {
		t.Fatalf("os.WriteFile(%q) error = %v", gatePath, err)
	}

	waitForModelsInPersistentCache(t, cachePath, []string{"fresh/model", "second/model"})

	got = switcher.AvailableModels(context.Background())
	if len(got) != 2 || got[0].Name != "fresh/model" || got[1].Name != "second/model" {
		t.Fatalf("AvailableModels() after refresh = %v, want refreshed cache", got)
	}
}

func TestAvailableModels_BackgroundRefreshFailurePreservesCache(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	writePersistentModelCache(t, cachePath, []core.ModelOption{{Name: "cached/model"}}, time.Now())
	countPath := filepath.Join(t.TempDir(), "refresh-count")
	gatePath := filepath.Join(t.TempDir(), "refresh-ready")
	bin := writeCountingModelsBin(t, countPath, gatePath, nil, "", 1)

	agent, err := New(map[string]any{
		"cmd":         bin,
		"cc_data_dir": dataDir,
		"cc_project":  "demo",
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	a := agent.(*Agent)

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "cached/model" {
		t.Fatalf("AvailableModels() = %v, want immediate cached result", got)
	}
	if err := os.WriteFile(gatePath, []byte("ok"), 0o644); err != nil {
		t.Fatalf("os.WriteFile(%q) error = %v", gatePath, err)
	}
	waitForFileContent(t, countPath, "1")

	cache, err := loadOpencodePersistentModelCache(cachePath)
	if err != nil {
		t.Fatalf("loadOpencodePersistentModelCache(%q) error = %v", cachePath, err)
	}
	if cache == nil || len(cache.Models) != 1 || cache.Models[0].Name != "cached/model" {
		t.Fatalf("persistent cache after failed refresh = %v, want cached/model preserved", cache)
	}
	inMemory := a.persistentModels()
	if len(inMemory) != 1 || inMemory[0].Name != "cached/model" {
		t.Fatalf("in-memory cache after failed refresh = %v, want cached/model preserved", inMemory)
	}
}

func TestAvailableModels_BackgroundRefreshSingleFlight(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	writePersistentModelCache(t, cachePath, []core.ModelOption{{Name: "cached/model"}}, time.Now())
	countPath := filepath.Join(t.TempDir(), "refresh-count")
	gatePath := filepath.Join(t.TempDir(), "refresh-ready")
	bin := writeCountingModelsBin(t, countPath, gatePath, []string{"fresh/model"}, "", 0)

	agent, err := New(map[string]any{
		"cmd":         bin,
		"cc_data_dir": dataDir,
		"cc_project":  "demo",
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	a := agent.(*Agent)

	for i := 0; i < 5; i++ {
		got := a.AvailableModels(context.Background())
		if len(got) != 1 || got[0].Name != "cached/model" {
			t.Fatalf("AvailableModels() call %d = %v, want cached/model", i, got)
		}
	}
	waitForFileContent(t, countPath, "1")
	if err := os.WriteFile(gatePath, []byte("ok"), 0o644); err != nil {
		t.Fatalf("os.WriteFile(%q) error = %v", gatePath, err)
	}
	waitForModelsInPersistentCache(t, cachePath, []string{"fresh/model"})
	data, err := os.ReadFile(countPath)
	if err != nil {
		t.Fatalf("os.ReadFile(%q) error = %v", countPath, err)
	}
	if strings.TrimSpace(string(data)) != "1" {
		t.Fatalf("refresh count = %q, want 1", strings.TrimSpace(string(data)))
	}
}

func TestStartInitialModelRefresh_UsesCurrentProviderWiring(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	writePersistentModelCache(t, cachePath, []core.ModelOption{{Name: "cached/model"}}, time.Now())
	countPath := filepath.Join(t.TempDir(), "refresh-count")
	gatePath := filepath.Join(t.TempDir(), "refresh-ready")
	bin := writeCountingModelsBin(t, countPath, gatePath, []string{"provider/model"}, "MODEL_DISCOVERY_TOKEN", 0)

	agent, err := New(map[string]any{
		"cmd":         bin,
		"cc_data_dir": dataDir,
		"cc_project":  "demo",
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	a := agent.(*Agent)
	a.SetProviders([]core.ProviderConfig{{
		Name: "provider-a",
		Env:  map[string]string{"MODEL_DISCOVERY_TOKEN": "present"},
	}})
	if !a.SetActiveProvider("provider-a") {
		t.Fatal("SetActiveProvider(provider-a) = false, want true")
	}

	a.StartInitialModelRefresh()
	waitForFileContent(t, countPath, "1")
	if err := os.WriteFile(gatePath, []byte("ok"), 0o644); err != nil {
		t.Fatalf("os.WriteFile(%q) error = %v", gatePath, err)
	}
	waitForModelsInPersistentCache(t, cachePath, []string{"provider/model"})
}

func TestStartInitialModelRefresh_PrewarmsColdStartCacheAfterProviderWiring(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	countPath := filepath.Join(t.TempDir(), "refresh-count")
	gatePath := filepath.Join(t.TempDir(), "refresh-ready")
	bin := writeCountingModelsBin(t, countPath, gatePath, []string{"provider/model"}, "MODEL_DISCOVERY_TOKEN", 0)

	agent, err := New(map[string]any{
		"cmd":         bin,
		"cc_data_dir": dataDir,
		"cc_project":  "demo",
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	a := agent.(*Agent)
	a.SetProviders([]core.ProviderConfig{{
		Name: "provider-a",
		Env:  map[string]string{"MODEL_DISCOVERY_TOKEN": "present"},
	}})
	if !a.SetActiveProvider("provider-a") {
		t.Fatal("SetActiveProvider(provider-a) = false, want true")
	}
	if cache, err := loadOpencodePersistentModelCache(cachePath); err != nil {
		t.Fatalf("loadOpencodePersistentModelCache(%q) error = %v", cachePath, err)
	} else if cache != nil {
		t.Fatalf("persistent cache before prewarm = %v, want nil", cache)
	}

	a.StartInitialModelRefresh()
	waitForFileContent(t, countPath, "1")
	if err := os.WriteFile(gatePath, []byte("ok"), 0o644); err != nil {
		t.Fatalf("os.WriteFile(%q) error = %v", gatePath, err)
	}
	waitForModelsInPersistentCache(t, cachePath, []string{"provider/model"})
}

func TestAvailableModels_DiscoveryUsesProviderEnv(t *testing.T) {
	countPath := filepath.Join(t.TempDir(), "refresh-count")
	bin := writeCountingModelsBin(t, countPath, "", []string{"provider/model"}, "MODEL_DISCOVERY_TOKEN", 0)
	a := &Agent{
		cmd:     bin,
		workDir: t.TempDir(),
		providers: []core.ProviderConfig{{
			Name:  "provider-a",
			Model: "provider/model",
			Env:   map[string]string{"MODEL_DISCOVERY_TOKEN": "present"},
		}},
		activeIdx: 0,
	}

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "provider/model" {
		t.Fatalf("AvailableModels() = %v, want provider/model discovered via provider env", got)
	}
}

func TestAvailableModels_PersistsProviderKeyOnColdStartDiscovery(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	bin := writeCountingModelsBin(t, "", "", []string{"provider/model"}, "MODEL_DISCOVERY_TOKEN", 0)
	a := &Agent{
		cmd:            bin,
		workDir:        t.TempDir(),
		modelCachePath: cachePath,
		providers: []core.ProviderConfig{{
			Name: "provider-a",
			Env:  map[string]string{"MODEL_DISCOVERY_TOKEN": "present"},
		}},
		activeIdx: 0,
	}

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "provider/model" {
		t.Fatalf("AvailableModels() = %v, want provider/model", got)
	}
	wantKey := providerCacheKeyOf(t, a)
	payload := readPersistentModelCachePayload(t, cachePath)
	if payload["provider_key"] != wantKey {
		t.Fatalf("provider_key = %v, want %q", payload["provider_key"], wantKey)
	}
}

func TestAvailableModels_IgnoresPersistentCacheForProviderMismatch(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	bin := writeFakeModelsBin(t, []string{"fresh/provider-b"}, 0)

	agent, err := New(map[string]any{
		"cmd":         bin,
		"cc_data_dir": dataDir,
		"cc_project":  "demo",
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	a := agent.(*Agent)
	a.SetProviders([]core.ProviderConfig{{Name: "provider-a"}, {Name: "provider-b"}})
	providerASnapshot := func() opencodeModelDiscoverySnapshot {
		a.activeIdx = 0
		providerCacheKeyOf(t, a)
		return a.modelDiscoverySnapshot()
	}()
	writePersistentModelCacheWithSnapshot(t, cachePath, providerASnapshot, []core.ModelOption{{Name: "cached/provider-a"}}, time.Now())
	a.persistentModelCache = &opencodePersistentModelCache{Models: []core.ModelOption{{Name: "cached/provider-a"}}, UpdatedAt: time.Now(), ProviderKey: providerASnapshot.providerKey, ContextKey: modelDiscoveryContextKey(providerASnapshot)}
	if !a.SetActiveProvider("provider-b") {
		t.Fatal("SetActiveProvider(provider-b) = false, want true")
	}

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "fresh/provider-b" {
		t.Fatalf("AvailableModels() = %v, want provider-mismatched cache ignored", got)
	}
	cache, err := loadOpencodePersistentModelCache(cachePath)
	if err != nil {
		t.Fatalf("loadOpencodePersistentModelCache(%q) error = %v", cachePath, err)
	}
	if cache == nil || len(cache.Models) != 1 || cache.Models[0].Name != "fresh/provider-b" {
		t.Fatalf("persistent cache after provider mismatch refresh = %v, want fresh/provider-b", cache)
	}
}

func TestAvailableModels_BackgroundRefreshPersistsProviderKey(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	gatePath := filepath.Join(t.TempDir(), "refresh-ready")
	bin := writeCountingModelsBin(t, "", gatePath, []string{"fresh/model"}, "MODEL_DISCOVERY_TOKEN", 0)

	a := &Agent{
		cmd:            bin,
		workDir:        t.TempDir(),
		modelCachePath: cachePath,
		providers: []core.ProviderConfig{{
			Name: "provider-a",
			Env:  map[string]string{"MODEL_DISCOVERY_TOKEN": "present"},
		}},
		activeIdx: 0,
	}
	providerAKey := providerCacheKeyOf(t, a)
	providerASnapshot := a.modelDiscoverySnapshot()
	writePersistentModelCacheWithSnapshot(t, cachePath, providerASnapshot, []core.ModelOption{{Name: "cached/model"}}, time.Now())
	a.persistentModelCache = &opencodePersistentModelCache{Models: []core.ModelOption{{Name: "cached/model"}}, UpdatedAt: time.Now(), ProviderKey: providerAKey, ContextKey: modelDiscoveryContextKey(providerASnapshot)}

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "cached/model" {
		t.Fatalf("AvailableModels() = %v, want cached/model", got)
	}
	if err := os.WriteFile(gatePath, []byte("ok"), 0o644); err != nil {
		t.Fatalf("os.WriteFile(%q) error = %v", gatePath, err)
	}
	waitForModelsInPersistentCache(t, cachePath, []string{"fresh/model"})
	payload := readPersistentModelCachePayload(t, cachePath)
	if payload["provider_key"] != providerAKey {
		t.Fatalf("provider_key = %v, want %q", payload["provider_key"], providerAKey)
	}
}

func TestAvailableModels_BackgroundRefreshUsesProviderSnapshot(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	countPath := filepath.Join(t.TempDir(), "refresh-count")
	gatePath := filepath.Join(t.TempDir(), "refresh-ready")
	bin := writeCountingModelsBin(t, countPath, gatePath, []string{"provider-a/model"}, "MODEL_DISCOVERY_TOKEN", 0)
	a := &Agent{
		cmd:            bin,
		workDir:        t.TempDir(),
		modelCachePath: cachePath,
		providers: []core.ProviderConfig{
			{Name: "provider-a", Env: map[string]string{"MODEL_DISCOVERY_TOKEN": "present"}},
			{Name: "provider-b", Env: map[string]string{}},
		},
		activeIdx: 0,
	}
	providerAKey := providerCacheKeyOf(t, a)
	providerASnapshot := a.modelDiscoverySnapshot()
	writePersistentModelCacheWithSnapshot(t, cachePath, providerASnapshot, []core.ModelOption{{Name: "cached/model"}}, time.Now())
	a.persistentModelCache = &opencodePersistentModelCache{Models: []core.ModelOption{{Name: "cached/model"}}, UpdatedAt: time.Now(), ProviderKey: providerAKey, ContextKey: modelDiscoveryContextKey(providerASnapshot)}

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "cached/model" {
		t.Fatalf("AvailableModels() = %v, want cached/model", got)
	}
	waitForFileContent(t, countPath, "1")
	if !a.SetActiveProvider("provider-b") {
		t.Fatal("SetActiveProvider(provider-b) = false, want true")
	}
	if err := os.WriteFile(gatePath, []byte("ok"), 0o644); err != nil {
		t.Fatalf("os.WriteFile(%q) error = %v", gatePath, err)
	}
	waitForModelsInPersistentCache(t, cachePath, []string{"provider-a/model"})
	payload := readPersistentModelCachePayload(t, cachePath)
	if payload["provider_key"] != providerAKey {
		t.Fatalf("provider_key after provider switch = %v, want %q snapshot", payload["provider_key"], providerAKey)
	}
}

func TestAvailableModels_IgnoresPersistentCacheForSameProviderNameDifferentConfig(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	bin := writeCountingModelsBin(t, "", "", []string{"fresh/provider-a"}, "MODEL_DISCOVERY_TOKEN", 0)
	a := &Agent{
		cmd:            bin,
		workDir:        t.TempDir(),
		modelCachePath: cachePath,
		providers: []core.ProviderConfig{{
			Name:    "provider-a",
			APIKey:  "new-secret",
			BaseURL: "https://new.example",
			Env:     map[string]string{"MODEL_DISCOVERY_TOKEN": "present", "EXTRA": "new"},
		}},
		activeIdx: 0,
	}
	stale := &Agent{providers: []core.ProviderConfig{{Name: "provider-a", APIKey: "old-secret", BaseURL: "https://old.example", Env: map[string]string{"MODEL_DISCOVERY_TOKEN": "old", "EXTRA": "old"}}}, activeIdx: 0}
	staleKey := providerCacheKeyOf(t, stale)
	staleSnapshot := stale.modelDiscoverySnapshot()
	writePersistentModelCacheWithSnapshot(t, cachePath, staleSnapshot, []core.ModelOption{{Name: "cached/stale"}}, time.Now())
	a.persistentModelCache = &opencodePersistentModelCache{Models: []core.ModelOption{{Name: "cached/stale"}}, UpdatedAt: time.Now(), ProviderKey: staleKey, ContextKey: modelDiscoveryContextKey(staleSnapshot)}

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "fresh/provider-a" {
		t.Fatalf("AvailableModels() = %v, want stale cache ignored for changed config", got)
	}
	payload := readPersistentModelCachePayload(t, cachePath)
	wantKey := providerCacheKeyOf(t, a)
	if payload["provider_key"] != wantKey {
		t.Fatalf("provider_key = %v, want %q", payload["provider_key"], wantKey)
	}
}

func TestAvailableModels_IgnoresPersistentCacheForWorkDirMismatch(t *testing.T) {
	dataDir := t.TempDir()
	cachePath := opencodeProjectModelCachePath(dataDir, "demo")
	countPath := filepath.Join(t.TempDir(), "refresh-count")
	workDirA := filepath.Join(t.TempDir(), "workspace-a")
	workDirB := filepath.Join(t.TempDir(), "workspace-b")
	if err := os.MkdirAll(workDirA, 0o755); err != nil {
		t.Fatalf("os.MkdirAll(%q) error = %v", workDirA, err)
	}
	if err := os.MkdirAll(workDirB, 0o755); err != nil {
		t.Fatalf("os.MkdirAll(%q) error = %v", workDirB, err)
	}
	bin := writeCountingModelsBin(t, countPath, "", []string{"fresh/workspace-b"}, "MODEL_DISCOVERY_TOKEN", 0)
	a := &Agent{
		cmd:            bin,
		workDir:        workDirB,
		modelCachePath: cachePath,
		providers: []core.ProviderConfig{{
			Name: "provider-a",
			Env:  map[string]string{"MODEL_DISCOVERY_TOKEN": "present"},
		}},
		activeIdx: 0,
	}
	stale := &Agent{
		workDir: workDirA,
		providers: []core.ProviderConfig{{
			Name: "provider-a",
			Env:  map[string]string{"MODEL_DISCOVERY_TOKEN": "present"},
		}},
		activeIdx: 0,
	}
	staleSnapshot := stale.modelDiscoverySnapshot()
	writePersistentModelCacheWithSnapshot(t, cachePath, staleSnapshot, []core.ModelOption{{Name: "cached/workspace-a"}}, time.Now())
	a.persistentModelCache = &opencodePersistentModelCache{
		Models:      []core.ModelOption{{Name: "cached/workspace-a"}},
		UpdatedAt:   time.Now(),
		ProviderKey: staleSnapshot.providerKey,
		ContextKey:  modelDiscoveryContextKey(staleSnapshot),
	}

	got := a.AvailableModels(context.Background())
	if len(got) != 1 || got[0].Name != "fresh/workspace-b" {
		t.Fatalf("AvailableModels() = %v, want stale cache ignored for changed workdir", got)
	}
	waitForFileContent(t, countPath, "1")
	payload := readPersistentModelCachePayload(t, cachePath)
	if payload["provider_key"] != staleSnapshot.providerKey {
		t.Fatalf("provider_key = %v, want %q", payload["provider_key"], staleSnapshot.providerKey)
	}
}

// ---------- DeleteSession tests ----------

// writeFakeDeleteBin writes a temporary shell script that acts as a fake opencode CLI.
// When invoked with "session delete <id>", it either succeeds (exitCode=0) or fails.
// If wantID is non-empty the script validates the session ID matches.
func writeFakeDeleteBin(t *testing.T, wantID string, exitCode int, stderr string) string {
	t.Helper()
	tmpDir := t.TempDir()
	name := filepath.Join(tmpDir, "fake-opencode")

	var body strings.Builder
	body.WriteString("#!/bin/sh\n")
	body.WriteString("if [ \"$1\" = \"session\" ] && [ \"$2\" = \"delete\" ]; then\n")
	if wantID != "" {
		fmt.Fprintf(&body, "  if [ \"$3\" != \"%s\" ]; then\n", wantID)
		fmt.Fprintf(&body, "    printf 'unexpected session id: %%s\\n' \"$3\" >&2\n")
		body.WriteString("    exit 1\n")
		body.WriteString("  fi\n")
	}
	if stderr != "" {
		fmt.Fprintf(&body, "  printf '%s\\n' >&2\n", stderr)
	}
	fmt.Fprintf(&body, "  exit %d\n", exitCode)
	body.WriteString("fi\n")
	body.WriteString("exit 0\n")

	if err := os.WriteFile(name, []byte(body.String()), 0755); err != nil {
		t.Fatal(err)
	}
	return name
}

// TestDeleteSession_Success verifies that DeleteSession calls
// `opencode session delete <id>` and returns nil on success.
func TestDeleteSession_Success(t *testing.T) {
	sessionID := "ses_abc123"
	bin := writeFakeDeleteBin(t, sessionID, 0, "")
	a := &Agent{cmd: bin, workDir: t.TempDir()}

	if err := a.DeleteSession(context.Background(), sessionID); err != nil {
		t.Fatalf("DeleteSession() unexpected error: %v", err)
	}
}

// TestDeleteSession_CLIError verifies that DeleteSession propagates CLI failures.
func TestDeleteSession_CLIError(t *testing.T) {
	bin := writeFakeDeleteBin(t, "", 1, "session not found")
	a := &Agent{cmd: bin, workDir: t.TempDir()}

	err := a.DeleteSession(context.Background(), "ses_missing")
	if err == nil {
		t.Fatal("DeleteSession() expected error, got nil")
	}
	if !strings.Contains(err.Error(), "ses_missing") {
		t.Errorf("error %q should mention the session ID", err.Error())
	}
}

// TestDeleteSession_ImplementsInterface is a compile-time check that Agent
// satisfies core.SessionDeleter.
var _ core.SessionDeleter = (*Agent)(nil)

// ---------- interface / compile-time checks ----------

// verify Agent implements core.Agent
var _ core.Agent = (*Agent)(nil)
