package launch

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
)

func TestOpenCodeIntegration(t *testing.T) {
	o := &OpenCode{}

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

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

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

func TestOpenCodeEdit(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	configDir := filepath.Join(tmpDir, ".config", "opencode")
	configPath := filepath.Join(configDir, "opencode.json")
	stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
	statePath := filepath.Join(stateDir, "model.json")

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

	t.Run("fresh install", func(t *testing.T) {
		cleanup()
		if err := o.Edit([]string{"llama3.2"}); err != nil {
			t.Fatal(err)
		}
		assertOpenCodeModelExists(t, configPath, "llama3.2")
		assertOpenCodeDefaultModel(t, configPath, "ollama/llama3.2")
		assertOpenCodeRecentModel(t, statePath, 0, "ollama", "llama3.2")
	})

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

	t.Run("preserve other models", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"models":{"mistral":{"name":"Mistral"}}}}}`), 0o644)
		if err := o.Edit([]string{"llama3.2"}); err != nil {
			t.Fatal(err)
		}
		assertOpenCodeModelExists(t, configPath, "mistral")
		assertOpenCodeModelExists(t, configPath, "llama3.2")
	})

	t.Run("update existing model", func(t *testing.T) {
		cleanup()
		o.Edit([]string{"llama3.2"})
		o.Edit([]string{"llama3.2"})
		assertOpenCodeModelExists(t, configPath, "llama3.2")
	})

	t.Run("preserve top-level keys", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"theme":"dark","keybindings":{}}`), 0o644)
		if err := o.Edit([]string{"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["keybindings"] == nil {
			t.Error("keybindings was removed")
		}
	})

	t.Run("model state - insert at index 0", func(t *testing.T) {
		cleanup()
		os.MkdirAll(stateDir, 0o755)
		os.WriteFile(statePath, []byte(`{"recent":[{"providerID":"anthropic","modelID":"claude"}],"favorite":[],"variant":{}}`), 0o644)
		if err := o.Edit([]string{"llama3.2"}); err != nil {
			t.Fatal(err)
		}
		assertOpenCodeRecentModel(t, statePath, 0, "ollama", "llama3.2")
		assertOpenCodeRecentModel(t, statePath, 1, "anthropic", "claude")
	})

	t.Run("model state - preserve favorites and variants", func(t *testing.T) {
		cleanup()
		os.MkdirAll(stateDir, 0o755)
		os.WriteFile(statePath, []byte(`{"recent":[],"favorite":[{"providerID":"x","modelID":"y"}],"variant":{"a":"b"}}`), 0o644)
		if err := o.Edit([]string{"llama3.2"}); err != nil {
			t.Fatal(err)
		}
		data, _ := os.ReadFile(statePath)
		var state map[string]any
		json.Unmarshal(data, &state)
		if len(state["favorite"].([]any)) != 1 {
			t.Error("favorite was modified")
		}
		if state["variant"].(map[string]any)["a"] != "b" {
			t.Error("variant was modified")
		}
	})

	t.Run("model state - deduplicate on re-add", func(t *testing.T) {
		cleanup()
		os.MkdirAll(stateDir, 0o755)
		os.WriteFile(statePath, []byte(`{"recent":[{"providerID":"ollama","modelID":"llama3.2"},{"providerID":"anthropic","modelID":"claude"}],"favorite":[],"variant":{}}`), 0o644)
		if err := o.Edit([]string{"llama3.2"}); err != nil {
			t.Fatal(err)
		}
		data, _ := os.ReadFile(statePath)
		var state map[string]any
		json.Unmarshal(data, &state)
		recent := state["recent"].([]any)
		if len(recent) != 2 {
			t.Errorf("expected 2 recent entries, got %d", len(recent))
		}
		assertOpenCodeRecentModel(t, statePath, 0, "ollama", "llama3.2")
	})

	t.Run("remove model", func(t *testing.T) {
		cleanup()
		// First add two models
		o.Edit([]string{"llama3.2", "mistral"})
		assertOpenCodeModelExists(t, configPath, "llama3.2")
		assertOpenCodeModelExists(t, configPath, "mistral")
		assertOpenCodeDefaultModel(t, configPath, "ollama/llama3.2")

		// Then remove one by only selecting the other
		o.Edit([]string{"llama3.2"})
		assertOpenCodeModelExists(t, configPath, "llama3.2")
		assertOpenCodeModelNotExists(t, configPath, "mistral")
		assertOpenCodeDefaultModel(t, configPath, "ollama/llama3.2")
	})

	t.Run("preserve user customizations on managed models", func(t *testing.T) {
		cleanup()
		if err := o.Edit([]string{"llama3.2"}); err != nil {
			t.Fatal(err)
		}

		// Add custom fields to the model entry (simulating user edits)
		data, _ := os.ReadFile(configPath)
		var cfg map[string]any
		json.Unmarshal(data, &cfg)
		provider := cfg["provider"].(map[string]any)
		ollama := provider["ollama"].(map[string]any)
		models := ollama["models"].(map[string]any)
		entry := models["llama3.2"].(map[string]any)
		entry["_myPref"] = "custom-value"
		entry["_myNum"] = 42
		configData, _ := json.MarshalIndent(cfg, "", "  ")
		os.WriteFile(configPath, configData, 0o644)

		// Re-run Edit — should preserve custom fields
		if err := o.Edit([]string{"llama3.2"}); err != nil {
			t.Fatal(err)
		}

		data, _ = os.ReadFile(configPath)
		json.Unmarshal(data, &cfg)
		provider = cfg["provider"].(map[string]any)
		ollama = provider["ollama"].(map[string]any)
		models = ollama["models"].(map[string]any)
		entry = models["llama3.2"].(map[string]any)

		if entry["_myPref"] != "custom-value" {
			t.Errorf("_myPref was lost: got %v", entry["_myPref"])
		}
		if entry["_myNum"] != float64(42) {
			t.Errorf("_myNum was lost: got %v", entry["_myNum"])
		}
		if v, ok := entry["_launch"].(bool); !ok || !v {
			t.Errorf("_launch marker missing or false: got %v", entry["_launch"])
		}
	})

	t.Run("migrate legacy [Ollama] suffix entries", func(t *testing.T) {
		cleanup()
		// Write a config with a legacy entry (has [Ollama] suffix but no _launch marker)
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"models":{"llama3.2":{"name":"llama3.2 [Ollama]"}}}}}`), 0o644)

		if err := o.Edit([]string{"llama3.2"}); err != nil {
			t.Fatal(err)
		}

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

		// _launch marker should be added
		if v, ok := entry["_launch"].(bool); !ok || !v {
			t.Errorf("_launch marker not added during migration: got %v", entry["_launch"])
		}
		// [Ollama] suffix should be stripped
		if name, ok := entry["name"].(string); !ok || name != "llama3.2" {
			t.Errorf("name suffix not stripped: got %q", entry["name"])
		}
	})

	t.Run("migrate Ollama (local) provider name", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"name":"Ollama (local)","npm":"@ai-sdk/openai-compatible","options":{"baseURL":"http://localhost:11434/v1"}}}}`), 0o644)

		if err := o.Edit([]string{"llama3.2"}); err != nil {
			t.Fatal(err)
		}

		data, _ := os.ReadFile(configPath)
		var cfg map[string]any
		json.Unmarshal(data, &cfg)
		provider := cfg["provider"].(map[string]any)
		ollama := provider["ollama"].(map[string]any)
		if ollama["name"] != "Ollama" {
			t.Errorf("provider name not migrated: got %q, want %q", ollama["name"], "Ollama")
		}
	})

	t.Run("preserve custom provider name", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"name":"My Custom Ollama","npm":"@ai-sdk/openai-compatible","options":{"baseURL":"http://localhost:11434/v1"}}}}`), 0o644)

		if err := o.Edit([]string{"llama3.2"}); err != nil {
			t.Fatal(err)
		}

		data, _ := os.ReadFile(configPath)
		var cfg map[string]any
		json.Unmarshal(data, &cfg)
		provider := cfg["provider"].(map[string]any)
		ollama := provider["ollama"].(map[string]any)
		if ollama["name"] != "My Custom Ollama" {
			t.Errorf("custom provider name was changed: got %q, want %q", ollama["name"], "My Custom Ollama")
		}
	})

	t.Run("remove model preserves non-ollama models", func(t *testing.T) {
		cleanup()
		os.MkdirAll(configDir, 0o755)
		// Add a non-Ollama model manually
		os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"models":{"external":{"name":"External Model"}}}}}`), 0o644)

		o.Edit([]string{"llama3.2"})
		assertOpenCodeModelExists(t, configPath, "llama3.2")
		assertOpenCodeModelExists(t, configPath, "external") // Should be preserved
	})
}

func TestOpenCodeEdit_SetsDefaultModelFromFirstSelection(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	configPath := filepath.Join(tmpDir, ".config", "opencode", "opencode.json")
	if err := o.Edit([]string{"kimi-k2.5:cloud", "llama3.2"}); err != nil {
		t.Fatal(err)
	}

	data, err := os.ReadFile(configPath)
	if err != nil {
		t.Fatal(err)
	}
	var cfg map[string]any
	if err := json.Unmarshal(data, &cfg); err != nil {
		t.Fatal(err)
	}
	if got, _ := cfg["model"].(string); got != "ollama/kimi-k2.5:cloud" {
		t.Fatalf("model = %q, want %q", got, "ollama/kimi-k2.5:cloud")
	}
}

func TestOpenCodeEdit_OverridesExistingDefaultModel(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	configDir := filepath.Join(tmpDir, ".config", "opencode")
	configPath := filepath.Join(configDir, "opencode.json")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(configPath, []byte(`{
		"$schema":"https://opencode.ai/config.json",
		"model":"ollama/old-model",
		"provider":{"ollama":{"models":{"old-model":{"name":"old-model"}}}}
	}`), 0o644); err != nil {
		t.Fatal(err)
	}

	if err := o.Edit([]string{"kimi-k2.5:cloud"}); err != nil {
		t.Fatal(err)
	}

	data, err := os.ReadFile(configPath)
	if err != nil {
		t.Fatal(err)
	}
	var cfg map[string]any
	if err := json.Unmarshal(data, &cfg); err != nil {
		t.Fatal(err)
	}
	if got, _ := cfg["model"].(string); got != "ollama/kimi-k2.5:cloud" {
		t.Fatalf("model = %q, want %q", got, "ollama/kimi-k2.5:cloud")
	}
}

func TestPrepareEditorIntegration_OpenCodeShowsBackupWarningWhenFilesChange(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	configDir := filepath.Join(tmpDir, ".config", "opencode")
	configPath := filepath.Join(configDir, "opencode.json")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatalf("failed to create config dir: %v", err)
	}
	if err := os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"models":{"old-model":{"name":"old-model","_launch":true}}}}}`), 0o644); err != nil {
		t.Fatalf("failed to seed config: %v", err)
	}

	stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
	statePath := filepath.Join(stateDir, "model.json")
	if err := os.MkdirAll(stateDir, 0o755); err != nil {
		t.Fatalf("failed to create state dir: %v", err)
	}
	if err := os.WriteFile(statePath, []byte(`{"recent":[{"providerID":"ollama","modelID":"old-model"}],"favorite":[],"variant":{}}`), 0o644); err != nil {
		t.Fatalf("failed to seed state: %v", err)
	}

	stderr := captureStderr(t, func() {
		if err := prepareEditorIntegration("opencode", o, o, []string{"llama3.2"}); err != nil {
			t.Fatalf("prepareEditorIntegration returned error: %v", err)
		}
	})
	if !strings.Contains(stderr, "OpenCode configuration has been modified. Backups are saved in") {
		t.Fatalf("expected OpenCode backup warning, got stderr: %q", stderr)
	}
}

func TestPrepareEditorIntegration_OpenCodeSkipsBackupWarningWhenFilesUnchanged(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	if err := o.Edit([]string{"llama3.2"}); err != nil {
		t.Fatalf("failed to seed opencode config: %v", err)
	}

	stderr := captureStderr(t, func() {
		if err := prepareEditorIntegration("opencode", o, o, []string{"llama3.2"}); err != nil {
			t.Fatalf("prepareEditorIntegration returned error: %v", err)
		}
	})
	if strings.Contains(stderr, "OpenCode configuration has been modified. Backups are saved in") {
		t.Fatalf("did not expect OpenCode backup warning, got stderr: %q", stderr)
	}
}

func assertOpenCodeModelExists(t *testing.T, path, model string) {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	var cfg map[string]any
	if err := json.Unmarshal(data, &cfg); err != nil {
		t.Fatal(err)
	}
	provider, ok := cfg["provider"].(map[string]any)
	if !ok {
		t.Fatal("provider not found")
	}
	ollama, ok := provider["ollama"].(map[string]any)
	if !ok {
		t.Fatal("ollama provider not found")
	}
	models, ok := ollama["models"].(map[string]any)
	if !ok {
		t.Fatal("models not found")
	}
	if models[model] == nil {
		t.Errorf("model %s not found", model)
	}
}

func assertOpenCodeModelNotExists(t *testing.T, path, model string) {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	var cfg map[string]any
	if err := json.Unmarshal(data, &cfg); err != nil {
		t.Fatal(err)
	}
	provider, ok := cfg["provider"].(map[string]any)
	if !ok {
		return // No provider means no model
	}
	ollama, ok := provider["ollama"].(map[string]any)
	if !ok {
		return // No ollama means no model
	}
	models, ok := ollama["models"].(map[string]any)
	if !ok {
		return // No models means no model
	}
	if models[model] != nil {
		t.Errorf("model %s should not exist but was found", model)
	}
}

func assertOpenCodeDefaultModel(t *testing.T, path, want string) {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	var cfg map[string]any
	if err := json.Unmarshal(data, &cfg); err != nil {
		t.Fatal(err)
	}
	got, _ := cfg["model"].(string)
	if got != want {
		t.Fatalf("default model = %q, want %q", got, want)
	}
}

func assertOpenCodeRecentModel(t *testing.T, path string, index int, providerID, modelID string) {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	var state map[string]any
	if err := json.Unmarshal(data, &state); err != nil {
		t.Fatal(err)
	}
	recent, ok := state["recent"].([]any)
	if !ok {
		t.Fatal("recent not found")
	}
	if index >= len(recent) {
		t.Fatalf("index %d out of range (len=%d)", index, len(recent))
	}
	entry, ok := recent[index].(map[string]any)
	if !ok {
		t.Fatal("entry is not a map")
	}
	if entry["providerID"] != providerID {
		t.Errorf("expected providerID %s, got %s", providerID, entry["providerID"])
	}
	if entry["modelID"] != modelID {
		t.Errorf("expected modelID %s, got %s", modelID, entry["modelID"])
	}
}

// Edge case tests for opencode.go

func TestOpenCodeEdit_CorruptedConfigJSON(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

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

	os.MkdirAll(configDir, 0o755)
	os.WriteFile(configPath, []byte(`{corrupted json content`), 0o644)

	// Should not panic - corrupted JSON should be treated as empty
	err := o.Edit([]string{"llama3.2"})
	if err != nil {
		t.Fatalf("Edit failed with corrupted config: %v", err)
	}

	// Verify valid JSON was created
	data, _ := os.ReadFile(configPath)
	var cfg map[string]any
	if err := json.Unmarshal(data, &cfg); err != nil {
		t.Errorf("resulting config is not valid JSON: %v", err)
	}
}

func TestOpenCodeEdit_CorruptedStateJSON(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
	statePath := filepath.Join(stateDir, "model.json")

	os.MkdirAll(stateDir, 0o755)
	os.WriteFile(statePath, []byte(`{corrupted state`), 0o644)

	err := o.Edit([]string{"llama3.2"})
	if err != nil {
		t.Fatalf("Edit failed with corrupted state: %v", err)
	}

	// Verify valid state was created
	data, _ := os.ReadFile(statePath)
	var state map[string]any
	if err := json.Unmarshal(data, &state); err != nil {
		t.Errorf("resulting state is not valid JSON: %v", err)
	}
}

func TestOpenCodeEdit_WrongTypeProvider(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

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

	os.MkdirAll(configDir, 0o755)
	os.WriteFile(configPath, []byte(`{"provider": "not a map"}`), 0o644)

	err := o.Edit([]string{"llama3.2"})
	if err != nil {
		t.Fatalf("Edit with wrong type provider failed: %v", err)
	}

	// Verify provider is now correct type
	data, _ := os.ReadFile(configPath)
	var cfg map[string]any
	json.Unmarshal(data, &cfg)

	provider, ok := cfg["provider"].(map[string]any)
	if !ok {
		t.Fatalf("provider should be map after setup, got %T", cfg["provider"])
	}
	if provider["ollama"] == nil {
		t.Error("ollama provider should be created")
	}
}

func TestOpenCodeEdit_WrongTypeRecent(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
	statePath := filepath.Join(stateDir, "model.json")

	os.MkdirAll(stateDir, 0o755)
	os.WriteFile(statePath, []byte(`{"recent": "not an array", "favorite": [], "variant": {}}`), 0o644)

	err := o.Edit([]string{"llama3.2"})
	if err != nil {
		t.Fatalf("Edit with wrong type recent failed: %v", err)
	}

	// The function should handle this gracefully
	data, _ := os.ReadFile(statePath)
	var state map[string]any
	json.Unmarshal(data, &state)

	// recent should be properly set after setup
	recent, ok := state["recent"].([]any)
	if !ok {
		t.Logf("Note: recent type after setup is %T (documenting behavior)", state["recent"])
	} else if len(recent) == 0 {
		t.Logf("Note: recent is empty (documenting behavior)")
	}
}

func TestOpenCodeEdit_EmptyModels(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

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

	os.MkdirAll(configDir, 0o755)
	originalContent := `{"provider":{"ollama":{"models":{"existing":{}}}}}`
	os.WriteFile(configPath, []byte(originalContent), 0o644)

	// Empty models should be no-op
	err := o.Edit([]string{})
	if err != nil {
		t.Fatalf("Edit with empty models failed: %v", err)
	}

	// Original content should be preserved (file not modified)
	data, _ := os.ReadFile(configPath)
	if string(data) != originalContent {
		t.Errorf("empty models should not modify file, but content changed")
	}
}

func TestOpenCodeEdit_SpecialCharsInModelName(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	// Model name with special characters (though unusual)
	specialModel := `model-with-"quotes"`

	err := o.Edit([]string{specialModel})
	if err != nil {
		t.Fatalf("Edit with special chars failed: %v", err)
	}

	// Verify it was stored correctly
	configDir := filepath.Join(tmpDir, ".config", "opencode")
	configPath := filepath.Join(configDir, "opencode.json")
	data, _ := os.ReadFile(configPath)

	var cfg map[string]any
	if err := json.Unmarshal(data, &cfg); err != nil {
		t.Fatalf("resulting config is invalid JSON: %v", err)
	}

	// Model should be accessible
	provider, _ := cfg["provider"].(map[string]any)
	ollama, _ := provider["ollama"].(map[string]any)
	models, _ := ollama["models"].(map[string]any)

	if models[specialModel] == nil {
		t.Errorf("model with special chars not found in config")
	}
}

func readOpenCodeModel(t *testing.T, configPath, model string) map[string]any {
	t.Helper()
	data, err := os.ReadFile(configPath)
	if err != nil {
		t.Fatal(err)
	}
	var cfg map[string]any
	json.Unmarshal(data, &cfg)
	provider := cfg["provider"].(map[string]any)
	ollama := provider["ollama"].(map[string]any)
	models := ollama["models"].(map[string]any)
	entry, ok := models[model].(map[string]any)
	if !ok {
		t.Fatalf("model %s not found in config", model)
	}
	return entry
}

func TestOpenCodeEdit_LocalModelNoLimit(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	configPath := filepath.Join(tmpDir, ".config", "opencode", "opencode.json")

	if err := o.Edit([]string{"llama3.2"}); err != nil {
		t.Fatal(err)
	}

	entry := readOpenCodeModel(t, configPath, "llama3.2")
	if entry["limit"] != nil {
		t.Errorf("local model should not have limit set, got %v", entry["limit"])
	}
}

func TestOpenCodeEdit_PreservesUserLimit(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

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

	// Set up a model with a user-configured limit
	os.MkdirAll(configDir, 0o755)
	os.WriteFile(configPath, []byte(`{
		"provider": {
			"ollama": {
				"models": {
					"llama3.2": {
						"name": "llama3.2",
						"_launch": true,
						"limit": {"context": 8192, "output": 4096}
					}
				}
			}
		}
	}`), 0o644)

	// Re-edit should preserve the user's limit (not delete it)
	if err := o.Edit([]string{"llama3.2"}); err != nil {
		t.Fatal(err)
	}

	entry := readOpenCodeModel(t, configPath, "llama3.2")
	limit, ok := entry["limit"].(map[string]any)
	if !ok {
		t.Fatal("user-configured limit was removed")
	}
	if limit["context"] != float64(8192) {
		t.Errorf("context limit changed: got %v, want 8192", limit["context"])
	}
	if limit["output"] != float64(4096) {
		t.Errorf("output limit changed: got %v, want 4096", limit["output"])
	}
}

func TestOpenCodeEdit_CloudModelLimitStructure(t *testing.T) {
	// Verify that when a cloud model entry has limits set (as Edit would do),
	// the structure matches what opencode expects and re-edit preserves them.
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

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

	expected := cloudModelLimits["glm-4.7"]

	// Simulate a cloud model that already has the limit set by a previous Edit
	os.MkdirAll(configDir, 0o755)
	os.WriteFile(configPath, []byte(fmt.Sprintf(`{
		"provider": {
			"ollama": {
				"models": {
					"glm-4.7:cloud": {
						"name": "glm-4.7:cloud",
						"_launch": true,
						"limit": {"context": %d, "output": %d}
					}
				}
			}
		}
	}`, expected.Context, expected.Output)), 0o644)

	// Re-edit should preserve the cloud model limit
	if err := o.Edit([]string{"glm-4.7:cloud"}); err != nil {
		t.Fatal(err)
	}

	entry := readOpenCodeModel(t, configPath, "glm-4.7:cloud")
	limit, ok := entry["limit"].(map[string]any)
	if !ok {
		t.Fatal("cloud model limit was removed on re-edit")
	}
	if limit["context"] != float64(expected.Context) {
		t.Errorf("context = %v, want %d", limit["context"], expected.Context)
	}
	if limit["output"] != float64(expected.Output) {
		t.Errorf("output = %v, want %d", limit["output"], expected.Output)
	}
}

func TestOpenCodeEdit_BackfillsCloudModelLimitOnExistingEntry(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/api/show" {
			fmt.Fprintf(w, `{"capabilities":[],"model_info":{},"remote_model":"glm-5"}`)
			return
		}
		w.WriteHeader(http.StatusNotFound)
	}))
	defer srv.Close()
	t.Setenv("OLLAMA_HOST", srv.URL)

	configDir := filepath.Join(tmpDir, ".config", "opencode")
	configPath := filepath.Join(configDir, "opencode.json")
	os.MkdirAll(configDir, 0o755)
	os.WriteFile(configPath, []byte(`{
		"provider": {
			"ollama": {
				"models": {
					"glm-5:cloud": {
						"name": "glm-5:cloud",
						"_launch": true
					}
				}
			}
		}
	}`), 0o644)

	if err := o.Edit([]string{"glm-5:cloud"}); err != nil {
		t.Fatal(err)
	}

	entry := readOpenCodeModel(t, configPath, "glm-5:cloud")
	limit, ok := entry["limit"].(map[string]any)
	if !ok {
		t.Fatal("cloud model limit was not added on re-edit")
	}
	if limit["context"] != float64(202_752) {
		t.Errorf("context = %v, want 202752", limit["context"])
	}
	if limit["output"] != float64(131_072) {
		t.Errorf("output = %v, want 131072", limit["output"])
	}
}

func TestLookupCloudModelLimit(t *testing.T) {
	tests := []struct {
		name        string
		wantOK      bool
		wantContext int
		wantOutput  int
	}{
		{"glm-4.7", false, 0, 0},
		{"glm-4.7:cloud", true, 202_752, 131_072},
		{"glm-5:cloud", true, 202_752, 131_072},
		{"glm-5.1:cloud", true, 202_752, 131_072},
		{"gemma4:31b-cloud", true, 262_144, 131_072},
		{"gpt-oss:120b-cloud", true, 131_072, 131_072},
		{"gpt-oss:20b-cloud", true, 131_072, 131_072},
		{"kimi-k2.5", false, 0, 0},
		{"kimi-k2.5:cloud", true, 262_144, 262_144},
		{"deepseek-v3.2", false, 0, 0},
		{"deepseek-v3.2:cloud", true, 163_840, 65_536},
		{"qwen3.5", false, 0, 0},
		{"qwen3.5:cloud", true, 262_144, 32_768},
		{"qwen3-coder:480b", false, 0, 0},
		{"qwen3-coder:480b:cloud", true, 262_144, 65_536},
		{"qwen3-coder-next:cloud", true, 262_144, 32_768},
		{"llama3.2", false, 0, 0},
		{"unknown-model:cloud", false, 0, 0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			l, ok := lookupCloudModelLimit(tt.name)
			if ok != tt.wantOK {
				t.Errorf("lookupCloudModelLimit(%q) ok = %v, want %v", tt.name, ok, tt.wantOK)
			}
			if ok {
				if l.Context != tt.wantContext {
					t.Errorf("context = %d, want %d", l.Context, tt.wantContext)
				}
				if l.Output != tt.wantOutput {
					t.Errorf("output = %d, want %d", l.Output, tt.wantOutput)
				}
			}
		})
	}
}

func TestFindOpenCode(t *testing.T) {
	t.Run("fallback to ~/.opencode/bin", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		// Ensure opencode is not on PATH
		t.Setenv("PATH", tmpDir)

		// Without the fallback binary, findOpenCode should fail
		if _, ok := findOpenCode(); ok {
			t.Fatal("findOpenCode should fail when binary is not on PATH or in fallback location")
		}

		// Create a fake binary at the curl install fallback location
		binDir := filepath.Join(tmpDir, ".opencode", "bin")
		os.MkdirAll(binDir, 0o755)
		name := "opencode"
		if runtime.GOOS == "windows" {
			name = "opencode.exe"
		}
		fakeBin := filepath.Join(binDir, name)
		os.WriteFile(fakeBin, []byte("#!/bin/sh\n"), 0o755)

		// Now findOpenCode should succeed via fallback
		path, ok := findOpenCode()
		if !ok {
			t.Fatal("findOpenCode should succeed with fallback binary")
		}
		if path != fakeBin {
			t.Errorf("findOpenCode = %q, want %q", path, fakeBin)
		}
	})
}

func TestOpenCodeModels_NoConfig(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	models := o.Models()
	if len(models) > 0 {
		t.Errorf("expected nil/empty for missing config, got %v", models)
	}
}
