package launch

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/ollama/ollama/api"
	"github.com/ollama/ollama/types/model"
)

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) {
	t.Run("builds config content with provider", func(t *testing.T) {
		setTestHome(t, t.TempDir())
		o := &OpenCode{}
		if err := o.Edit(testLaunchModels("llama3.2")); err != nil {
			t.Fatal(err)
		}

		var cfg map[string]any
		if err := json.Unmarshal([]byte(o.configContent), &cfg); err != nil {
			t.Fatalf("configContent is not valid JSON: %v", err)
		}

		// Verify provider structure
		provider, _ := cfg["provider"].(map[string]any)
		ollama, _ := provider["ollama"].(map[string]any)
		if ollama["name"] != "Ollama" {
			t.Errorf("provider name = %v, want Ollama", ollama["name"])
		}
		if ollama["npm"] != "@ai-sdk/openai-compatible" {
			t.Errorf("npm = %v, want @ai-sdk/openai-compatible", ollama["npm"])
		}

		// Verify model exists
		models, _ := ollama["models"].(map[string]any)
		if models["llama3.2"] == nil {
			t.Error("model llama3.2 not found in config content")
		}

		// Verify default model
		if cfg["model"] != "ollama/llama3.2" {
			t.Errorf("model = %v, want ollama/llama3.2", cfg["model"])
		}
	})

	t.Run("multiple models", func(t *testing.T) {
		setTestHome(t, t.TempDir())
		o := &OpenCode{}
		if err := o.Edit(testLaunchModels("llama3.2", "qwen3:32b")); err != nil {
			t.Fatal(err)
		}

		var cfg map[string]any
		json.Unmarshal([]byte(o.configContent), &cfg)
		provider, _ := cfg["provider"].(map[string]any)
		ollama, _ := provider["ollama"].(map[string]any)
		models, _ := ollama["models"].(map[string]any)

		if models["llama3.2"] == nil {
			t.Error("model llama3.2 not found")
		}
		if models["qwen3:32b"] == nil {
			t.Error("model qwen3:32b not found")
		}
		// First model should be the default
		if cfg["model"] != "ollama/llama3.2" {
			t.Errorf("default model = %v, want ollama/llama3.2", cfg["model"])
		}
	})

	t.Run("empty models is no-op", func(t *testing.T) {
		setTestHome(t, t.TempDir())
		o := &OpenCode{}
		if err := o.Edit(testLaunchModels()); err != nil {
			t.Fatal(err)
		}
		if o.configContent != "" {
			t.Errorf("expected empty configContent for no models, got %s", o.configContent)
		}
	})

	t.Run("does not write config files", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)
		o := &OpenCode{}
		o.Edit(testLaunchModels("llama3.2"))

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

		if _, err := os.Stat(filepath.Join(configDir, "opencode.json")); !os.IsNotExist(err) {
			t.Error("opencode.json should not be created")
		}
		if _, err := os.Stat(filepath.Join(configDir, "opencode.jsonc")); !os.IsNotExist(err) {
			t.Error("opencode.jsonc should not be created")
		}
	})

	t.Run("cloud model has limits", func(t *testing.T) {
		setTestHome(t, t.TempDir())
		o := &OpenCode{}
		if err := o.Edit(testLaunchModels("glm-4.7:cloud")); err != nil {
			t.Fatal(err)
		}

		var cfg map[string]any
		json.Unmarshal([]byte(o.configContent), &cfg)
		provider, _ := cfg["provider"].(map[string]any)
		ollama, _ := provider["ollama"].(map[string]any)
		models, _ := ollama["models"].(map[string]any)
		entry, _ := models["glm-4.7:cloud"].(map[string]any)

		limit, ok := entry["limit"].(map[string]any)
		if !ok {
			t.Fatal("cloud model should have limit set")
		}
		expected := cloudModelLimits["glm-4.7"]
		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)
		}
	})

	t.Run("local model has no limits", func(t *testing.T) {
		setTestHome(t, t.TempDir())
		o := &OpenCode{}
		o.Edit(testLaunchModels("llama3.2"))

		var cfg map[string]any
		json.Unmarshal([]byte(o.configContent), &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["limit"] != nil {
			t.Errorf("local model should not have limit, got %v", entry["limit"])
		}
	})

	t.Run("vision model gets image input modalities", func(t *testing.T) {
		models := buildModelEntries([]LaunchModel{{Name: "gemma4:26b", Capabilities: []model.Capability{"vision"}}})
		entry, _ := models["gemma4:26b"].(map[string]any)
		modalities, _ := entry["modalities"].(map[string]any)
		input, _ := modalities["input"].([]string)
		output, _ := modalities["output"].([]string)

		if len(input) != 2 || input[0] != "text" || input[1] != "image" {
			t.Fatalf("modalities.input = %v, want [text image]", input)
		}
		if len(output) != 1 || output[0] != "text" {
			t.Fatalf("modalities.output = %v, want [text]", output)
		}
	})

	t.Run("thinking model gets on off reasoning variants", func(t *testing.T) {
		models := buildModelEntries([]LaunchModel{{Name: "thinking-model", Capabilities: []model.Capability{model.CapabilityThinking}}})
		entry, _ := models["thinking-model"].(map[string]any)

		if entry["reasoning"] != true {
			t.Fatalf("reasoning = %v, want true", entry["reasoning"])
		}
		variants, _ := entry["variants"].(map[string]any)
		none, _ := variants["none"].(map[string]any)
		if none["reasoningEffort"] != "none" {
			t.Fatalf("variants.none.reasoningEffort = %v, want none", none["reasoningEffort"])
		}
		for _, level := range []string{"low", "medium", "high"} {
			variant, _ := variants[level].(map[string]any)
			if variant["disabled"] != true {
				t.Fatalf("variants.%s.disabled = %v, want true", level, variant["disabled"])
			}
		}
	})

	t.Run("gpt oss gets reasoning level variants", func(t *testing.T) {
		models := buildModelEntries([]LaunchModel{{Name: "gpt-oss:120b-cloud", Capabilities: []model.Capability{model.CapabilityThinking}}})
		entry, _ := models["gpt-oss:120b-cloud"].(map[string]any)
		options, _ := entry["options"].(map[string]any)

		if options["reasoningEffort"] != "medium" {
			t.Fatalf("options.reasoningEffort = %v, want medium", options["reasoningEffort"])
		}
		variants, _ := entry["variants"].(map[string]any)
		for _, level := range []string{"low", "medium", "high", "max"} {
			variant, _ := variants[level].(map[string]any)
			if variant["reasoningEffort"] != level {
				t.Fatalf("variants.%s.reasoningEffort = %v, want %s", level, variant["reasoningEffort"], level)
			}
		}
	})

	t.Run("gpt oss family gets reasoning level variants", func(t *testing.T) {
		models := buildModelEntries([]LaunchModel{{Name: "reasoning-model", Capabilities: []model.Capability{model.CapabilityThinking}, Details: api.ModelDetails{Families: []string{"gptoss"}}}})
		entry, _ := models["reasoning-model"].(map[string]any)
		variants, _ := entry["variants"].(map[string]any)
		max, _ := variants["max"].(map[string]any)

		if max["reasoningEffort"] != "max" {
			t.Fatalf("variants.max.reasoningEffort = %v, want max", max["reasoningEffort"])
		}
	})
}

func TestBuildModelEntries(t *testing.T) {
	t.Run("defaults to model name without capabilities", func(t *testing.T) {
		models := buildModelEntries(testLaunchModels("llama3.2"))
		entry, _ := models["llama3.2"].(map[string]any)
		if entry["name"] != "llama3.2" {
			t.Fatalf("name = %v, want llama3.2", entry["name"])
		}
		if _, ok := entry["modalities"]; ok {
			t.Fatalf("modalities should not be set without capabilities, got %v", entry["modalities"])
		}
	})

	t.Run("uses context and output limits from metadata", func(t *testing.T) {
		models := buildModelEntries([]LaunchModel{{Name: "glm-5:cloud", ContextLength: 202_752, MaxOutputTokens: 131_072}})
		entry, _ := models["glm-5:cloud"].(map[string]any)
		limit, _ := entry["limit"].(map[string]any)
		if limit["context"] != 202_752 || limit["output"] != 131_072 {
			t.Fatalf("limit = %v, want context/output", limit)
		}
	})

	t.Run("omits context-only limits", func(t *testing.T) {
		models := buildModelEntries([]LaunchModel{{Name: "qwen2.5:0.5b", ContextLength: 32768}})
		entry, _ := models["qwen2.5:0.5b"].(map[string]any)
		if _, ok := entry["limit"]; ok {
			t.Fatalf("limit should be omitted when output limit is unknown, got %v", entry["limit"])
		}
	})
}

func TestOpenCodeModels_ReturnsNil(t *testing.T) {
	o := &OpenCode{}
	if models := o.Models(); models != nil {
		t.Errorf("Models() = %v, want nil", models)
	}
}

func TestOpenCodePaths(t *testing.T) {
	t.Run("returns nil when model.json does not exist", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		o := &OpenCode{}
		if paths := o.Paths(); paths != nil {
			t.Errorf("Paths() = %v, want nil", paths)
		}
	})

	t.Run("returns model.json path when it exists", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		os.WriteFile(filepath.Join(stateDir, "model.json"), []byte(`{}`), 0o644)

		o := &OpenCode{}
		paths := o.Paths()
		if len(paths) != 1 {
			t.Fatalf("Paths() returned %d paths, want 1", len(paths))
		}
		if paths[0] != filepath.Join(stateDir, "model.json") {
			t.Errorf("Paths() = %v, want %v", paths[0], filepath.Join(stateDir, "model.json"))
		}
	})
}

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) {
	oldGOOS := openCodeGOOS
	t.Cleanup(func() { openCodeGOOS = oldGOOS })

	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)
		openCodeGOOS = runtime.GOOS

		// 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 TestEnsureOpenCodeInstalled(t *testing.T) {
	oldGOOS := openCodeGOOS
	t.Cleanup(func() { openCodeGOOS = oldGOOS })

	withConfirm := func(t *testing.T, fn func(prompt string) (bool, error)) {
		t.Helper()
		oldConfirm := DefaultConfirmPrompt
		DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
			return fn(prompt)
		}
		t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
	}

	t.Run("already installed", func(t *testing.T) {
		setTestHome(t, t.TempDir())
		tmpDir := t.TempDir()
		t.Setenv("PATH", tmpDir)
		openCodeGOOS = runtime.GOOS
		writeFakeBinary(t, tmpDir, "opencode")

		withConfirm(t, func(prompt string) (bool, error) {
			t.Fatalf("did not expect prompt, got %q", prompt)
			return false, nil
		})

		bin, err := ensureOpenCodeInstalled()
		if err != nil {
			t.Fatalf("ensureOpenCodeInstalled() error = %v", err)
		}
		if filepath.Base(bin) == "" {
			t.Fatalf("expected opencode binary path, got %q", bin)
		}
	})

	t.Run("missing dependencies", func(t *testing.T) {
		setTestHome(t, t.TempDir())
		t.Setenv("PATH", t.TempDir())
		openCodeGOOS = "linux"

		withConfirm(t, func(prompt string) (bool, error) {
			t.Fatalf("did not expect prompt, got %q", prompt)
			return false, nil
		})

		_, err := ensureOpenCodeInstalled()
		if err == nil || !strings.Contains(err.Error(), "required dependencies are missing") {
			t.Fatalf("expected missing dependency error, got %v", err)
		}
	})

	t.Run("missing and user declines install", func(t *testing.T) {
		setTestHome(t, t.TempDir())
		tmpDir := t.TempDir()
		t.Setenv("PATH", tmpDir)
		openCodeGOOS = "linux"
		writeFakeBinary(t, tmpDir, "curl")
		writeFakeBinary(t, tmpDir, "bash")

		withConfirm(t, func(prompt string) (bool, error) {
			if !strings.Contains(prompt, "OpenCode is not installed.") {
				t.Fatalf("unexpected prompt: %q", prompt)
			}
			return false, nil
		})

		_, err := ensureOpenCodeInstalled()
		if err == nil || !strings.Contains(err.Error(), "installation cancelled") {
			t.Fatalf("expected cancellation error, got %v", err)
		}
	})

	t.Run("missing and user confirms unix install succeeds", func(t *testing.T) {
		if runtime.GOOS == "windows" {
			t.Skip("uses POSIX shell fake binaries")
		}

		homeDir := t.TempDir()
		setTestHome(t, homeDir)
		tmpDir := t.TempDir()
		t.Setenv("PATH", tmpDir)
		openCodeGOOS = "linux"
		writeFakeBinary(t, tmpDir, "curl")

		installLog := filepath.Join(tmpDir, "bash.log")
		opencodePath := filepath.Join(homeDir, ".opencode", "bin", "opencode")
		bashScript := fmt.Sprintf(`#!/bin/sh
echo "$@" >> %q
if [ "$1" = "-c" ]; then
  /bin/mkdir -p %q
  /bin/cat > %q <<'EOS'
#!/bin/sh
exit 0
EOS
  /bin/chmod +x %q
fi
exit 0
`, installLog, filepath.Dir(opencodePath), opencodePath, opencodePath)
		if err := os.WriteFile(filepath.Join(tmpDir, "bash"), []byte(bashScript), 0o755); err != nil {
			t.Fatalf("failed to write fake bash: %v", err)
		}

		withConfirm(t, func(prompt string) (bool, error) {
			return true, nil
		})

		bin, err := ensureOpenCodeInstalled()
		if err != nil {
			t.Fatalf("ensureOpenCodeInstalled() error = %v", err)
		}
		if bin != opencodePath {
			t.Fatalf("bin = %q, want %q", bin, opencodePath)
		}

		logData, err := os.ReadFile(installLog)
		if err != nil {
			t.Fatalf("failed to read install log: %v", err)
		}
		if !strings.Contains(string(logData), openCodeInstallScript) {
			t.Fatalf("expected opencode install script in log, got:\n%s", string(logData))
		}
	})

	t.Run("missing and user confirms windows install succeeds", func(t *testing.T) {
		if runtime.GOOS == "windows" {
			t.Skip("uses POSIX shell fake binaries")
		}

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

		installLog := filepath.Join(tmpDir, "npm.log")
		opencodePath := filepath.Join(homeDir, ".opencode", "bin", "opencode.exe")
		npmScript := fmt.Sprintf(`#!/bin/sh
echo "$@" >> %q
/bin/mkdir -p %q
/bin/cat > %q <<'EOS'
@echo off
exit /b 0
EOS
/bin/chmod +x %q
exit 0
`, installLog, filepath.Dir(opencodePath), opencodePath, opencodePath)
		if err := os.WriteFile(filepath.Join(tmpDir, "npm"), []byte(npmScript), 0o755); err != nil {
			t.Fatalf("failed to write fake npm: %v", err)
		}

		withConfirm(t, func(prompt string) (bool, error) {
			return true, nil
		})

		bin, err := ensureOpenCodeInstalled()
		if err != nil {
			t.Fatalf("ensureOpenCodeInstalled() error = %v", err)
		}
		if bin != opencodePath {
			t.Fatalf("bin = %q, want %q", bin, opencodePath)
		}

		logData, err := os.ReadFile(installLog)
		if err != nil {
			t.Fatalf("failed to read install log: %v", err)
		}
		if !strings.Contains(string(logData), "install -g opencode-ai@latest") {
			t.Fatalf("expected npm install command in log, got:\n%s", string(logData))
		}
	})

	t.Run("install command fails", func(t *testing.T) {
		if runtime.GOOS == "windows" {
			t.Skip("uses POSIX shell fake binaries")
		}

		setTestHome(t, t.TempDir())
		tmpDir := t.TempDir()
		t.Setenv("PATH", tmpDir)
		openCodeGOOS = "linux"
		writeFakeBinary(t, tmpDir, "curl")
		if err := os.WriteFile(filepath.Join(tmpDir, "bash"), []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
			t.Fatalf("failed to write fake bash: %v", err)
		}

		withConfirm(t, func(prompt string) (bool, error) {
			return true, nil
		})

		_, err := ensureOpenCodeInstalled()
		if err == nil || !strings.Contains(err.Error(), "failed to install opencode") {
			t.Fatalf("expected install failure error, got %v", err)
		}
	})
}

func TestOpenCodeInstallerCommand(t *testing.T) {
	tests := []struct {
		name      string
		goos      string
		wantBin   string
		wantParts []string
		wantErr   bool
	}{
		{
			name:      "linux",
			goos:      "linux",
			wantBin:   "bash",
			wantParts: []string{"-c", "set -o pipefail", "https://opencode.ai/install"},
		},
		{
			name:      "darwin",
			goos:      "darwin",
			wantBin:   "bash",
			wantParts: []string{"-c", "set -o pipefail", "https://opencode.ai/install"},
		},
		{
			name:      "windows",
			goos:      "windows",
			wantBin:   "npm",
			wantParts: []string{"install", "-g", "opencode-ai@latest"},
		},
		{
			name:    "unsupported",
			goos:    "plan9",
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			bin, args, err := openCodeInstallerCommand(tt.goos)
			if tt.wantErr {
				if err == nil {
					t.Fatal("expected error")
				}
				return
			}
			if err != nil {
				t.Fatalf("openCodeInstallerCommand() error = %v", err)
			}
			if bin != tt.wantBin {
				t.Fatalf("bin = %q, want %q", bin, tt.wantBin)
			}
			joined := strings.Join(args, " ")
			for _, want := range tt.wantParts {
				if !strings.Contains(joined, want) {
					t.Fatalf("args %q missing %q", joined, want)
				}
			}
		})
	}
}

// Verify that the BackfillsCloudModelLimitOnExistingEntry test from the old
// file-based approach is covered by the new inline config approach.
func TestOpenCodeEdit_CloudModelLimitStructure(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	expected := cloudModelLimits["glm-4.7"]

	if err := o.Edit(testLaunchModels("glm-4.7:cloud")); err != nil {
		t.Fatal(err)
	}

	var cfg map[string]any
	json.Unmarshal([]byte(o.configContent), &cfg)
	provider, _ := cfg["provider"].(map[string]any)
	ollama, _ := provider["ollama"].(map[string]any)
	models, _ := ollama["models"].(map[string]any)
	entry, _ := models["glm-4.7:cloud"].(map[string]any)

	limit, ok := entry["limit"].(map[string]any)
	if !ok {
		t.Fatal("cloud model limit was not set")
	}
	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_SpecialCharsInModelName(t *testing.T) {
	o := &OpenCode{}
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	specialModel := `model-with-"quotes"`

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

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

	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 TestReadModelJSONModels(t *testing.T) {
	t.Run("reads ollama models from model.json", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		state := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
				map[string]any{"providerID": "ollama", "modelID": "qwen3:32b"},
			},
		}
		data, _ := json.MarshalIndent(state, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		models := readModelJSONModels()
		if len(models) != 2 {
			t.Fatalf("got %d models, want 2", len(models))
		}
		if models[0] != "llama3.2" || models[1] != "qwen3:32b" {
			t.Errorf("got %v, want [llama3.2 qwen3:32b]", models)
		}
	})

	t.Run("skips non-ollama providers", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		state := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "openai", "modelID": "gpt-4"},
				map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
			},
		}
		data, _ := json.MarshalIndent(state, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

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

	t.Run("returns nil when file does not exist", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		if models := readModelJSONModels(); models != nil {
			t.Errorf("got %v, want nil", models)
		}
	})

	t.Run("returns nil for corrupt JSON", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		os.WriteFile(filepath.Join(stateDir, "model.json"), []byte(`{corrupt`), 0o644)

		if models := readModelJSONModels(); models != nil {
			t.Errorf("got %v, want nil", models)
		}
	})
}

func TestOpenCodeResolveContent(t *testing.T) {
	t.Run("returns Edit's content when set", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		o := &OpenCode{}
		if err := o.Edit(testLaunchModels("gemma4")); err != nil {
			t.Fatal(err)
		}
		editContent := o.configContent

		// Write a different model.json — should be ignored
		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		state := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "ollama", "modelID": "different-model"},
			},
		}
		data, _ := json.MarshalIndent(state, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		got := o.resolveContent("gemma4", nil)
		if got != editContent {
			t.Errorf("resolveContent returned different content than Edit set\ngot:  %s\nwant: %s", got, editContent)
		}
	})

	t.Run("falls back to model.json when Edit was not called", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		state := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
				map[string]any{"providerID": "ollama", "modelID": "qwen3:32b"},
			},
		}
		data, _ := json.MarshalIndent(state, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		o := &OpenCode{}
		content := o.resolveContent("llama3.2", nil)
		if content == "" {
			t.Fatal("resolveContent returned empty")
		}

		var cfg map[string]any
		json.Unmarshal([]byte(content), &cfg)
		if cfg["model"] != "ollama/llama3.2" {
			t.Errorf("primary = %v, want ollama/llama3.2", cfg["model"])
		}
		provider, _ := cfg["provider"].(map[string]any)
		ollama, _ := provider["ollama"].(map[string]any)
		cfgModels, _ := ollama["models"].(map[string]any)
		if cfgModels["llama3.2"] == nil || cfgModels["qwen3:32b"] == nil {
			t.Errorf("expected both models in config, got %v", cfgModels)
		}
	})

	t.Run("uses requested model as primary even when not first in model.json", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		state := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
				map[string]any{"providerID": "ollama", "modelID": "qwen3:32b"},
			},
		}
		data, _ := json.MarshalIndent(state, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		o := &OpenCode{}
		content := o.resolveContent("qwen3:32b", nil)

		var cfg map[string]any
		json.Unmarshal([]byte(content), &cfg)
		if cfg["model"] != "ollama/qwen3:32b" {
			t.Errorf("primary = %v, want ollama/qwen3:32b", cfg["model"])
		}
	})

	t.Run("injects requested model when missing from model.json", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		state := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
			},
		}
		data, _ := json.MarshalIndent(state, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		o := &OpenCode{}
		content := o.resolveContent("gemma4", nil)

		var cfg map[string]any
		json.Unmarshal([]byte(content), &cfg)
		provider, _ := cfg["provider"].(map[string]any)
		ollama, _ := provider["ollama"].(map[string]any)
		cfgModels, _ := ollama["models"].(map[string]any)
		if cfgModels["gemma4"] == nil {
			t.Error("requested model gemma4 not injected into config")
		}
		if cfg["model"] != "ollama/gemma4" {
			t.Errorf("primary = %v, want ollama/gemma4", cfg["model"])
		}
	})

	t.Run("returns empty when no model.json and no model param", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		o := &OpenCode{}
		if got := o.resolveContent("", nil); got != "" {
			t.Errorf("resolveContent(\"\") = %q, want empty", got)
		}
	})

	t.Run("uses run model metadata when Edit was not called", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		state := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
			},
		}
		data, _ := json.MarshalIndent(state, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		o := &OpenCode{}
		content := o.resolveContent("gemma4", []LaunchModel{
			{
				Name:            "gemma4",
				Capabilities:    []model.Capability{model.CapabilityVision},
				ContextLength:   65_536,
				MaxOutputTokens: 8_192,
			},
		})
		if content == "" {
			t.Fatal("resolveContent returned empty")
		}

		var cfg map[string]any
		json.Unmarshal([]byte(content), &cfg)
		provider, _ := cfg["provider"].(map[string]any)
		ollama, _ := provider["ollama"].(map[string]any)
		cfgModels, _ := ollama["models"].(map[string]any)
		entry, _ := cfgModels["gemma4"].(map[string]any)
		limit, _ := entry["limit"].(map[string]any)
		if limit["context"] != float64(65_536) || limit["output"] != float64(8_192) {
			t.Fatalf("limit = %v, want context/output from launch metadata", limit)
		}
		if _, ok := entry["modalities"].(map[string]any); !ok {
			t.Fatalf("modalities should be set from launch metadata, got %v", entry["modalities"])
		}
		if cfgModels["llama3.2"] == nil {
			t.Fatalf("state model missing from fallback config: %v", cfgModels)
		}
	})

	t.Run("does not mutate configContent on fallback", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		state := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
			},
		}
		data, _ := json.MarshalIndent(state, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		o := &OpenCode{}
		_ = o.resolveContent("llama3.2", nil)
		if o.configContent != "" {
			t.Errorf("resolveContent should not mutate configContent, got %q", o.configContent)
		}
	})
}

func TestBuildInlineConfig(t *testing.T) {
	t.Run("returns error for empty primary", func(t *testing.T) {
		if _, err := buildInlineConfig(LaunchModel{}, testLaunchModels("llama3.2")); err == nil {
			t.Error("expected error for empty primary")
		}
	})

	t.Run("returns error for empty models", func(t *testing.T) {
		if _, err := buildInlineConfig(fallbackLaunchModel("llama3.2"), nil); err == nil {
			t.Error("expected error for empty models")
		}
	})

	t.Run("primary differs from first model in list", func(t *testing.T) {
		content, err := buildInlineConfig(fallbackLaunchModel("qwen3:32b"), testLaunchModels("llama3.2", "qwen3:32b"))
		if err != nil {
			t.Fatal(err)
		}
		var cfg map[string]any
		json.Unmarshal([]byte(content), &cfg)
		if cfg["model"] != "ollama/qwen3:32b" {
			t.Errorf("primary = %v, want ollama/qwen3:32b", cfg["model"])
		}
	})
}

func TestOpenCodeEdit_PreservesRecentEntries(t *testing.T) {
	t.Run("prepends new models to existing recent", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		initial := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "ollama", "modelID": "old-A"},
				map[string]any{"providerID": "ollama", "modelID": "old-B"},
			},
		}
		data, _ := json.MarshalIndent(initial, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		o := &OpenCode{}
		if err := o.Edit(testLaunchModels("new-X")); err != nil {
			t.Fatal(err)
		}

		stored, _ := os.ReadFile(filepath.Join(stateDir, "model.json"))
		var state map[string]any
		json.Unmarshal(stored, &state)
		recent, _ := state["recent"].([]any)

		if len(recent) != 3 {
			t.Fatalf("expected 3 entries, got %d", len(recent))
		}
		first, _ := recent[0].(map[string]any)
		if first["modelID"] != "new-X" {
			t.Errorf("first entry = %v, want new-X", first["modelID"])
		}
	})

	t.Run("prepends multiple new models in order", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		initial := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "ollama", "modelID": "old-A"},
				map[string]any{"providerID": "ollama", "modelID": "old-B"},
			},
		}
		data, _ := json.MarshalIndent(initial, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		o := &OpenCode{}
		if err := o.Edit(testLaunchModels("X", "Y", "Z")); err != nil {
			t.Fatal(err)
		}

		stored, _ := os.ReadFile(filepath.Join(stateDir, "model.json"))
		var state map[string]any
		json.Unmarshal(stored, &state)
		recent, _ := state["recent"].([]any)

		want := []string{"X", "Y", "Z", "old-A", "old-B"}
		if len(recent) != len(want) {
			t.Fatalf("expected %d entries, got %d", len(want), len(recent))
		}
		for i, w := range want {
			e, _ := recent[i].(map[string]any)
			if e["modelID"] != w {
				t.Errorf("recent[%d] = %v, want %v", i, e["modelID"], w)
			}
		}
	})

	t.Run("preserves non-ollama entries", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		initial := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "openai", "modelID": "gpt-4"},
				map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
			},
		}
		data, _ := json.MarshalIndent(initial, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		o := &OpenCode{}
		if err := o.Edit(testLaunchModels("qwen3:32b")); err != nil {
			t.Fatal(err)
		}

		stored, _ := os.ReadFile(filepath.Join(stateDir, "model.json"))
		var state map[string]any
		json.Unmarshal(stored, &state)
		recent, _ := state["recent"].([]any)

		// Should have: qwen3:32b (new), gpt-4 (preserved openai), llama3.2 (preserved ollama)
		var foundOpenAI bool
		for _, entry := range recent {
			e, _ := entry.(map[string]any)
			if e["providerID"] == "openai" && e["modelID"] == "gpt-4" {
				foundOpenAI = true
			}
		}
		if !foundOpenAI {
			t.Errorf("non-ollama gpt-4 entry was not preserved, got %v", recent)
		}
	})

	t.Run("deduplicates ollama models being re-added", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)
		initial := map[string]any{
			"recent": []any{
				map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
			},
		}
		data, _ := json.MarshalIndent(initial, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

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

		stored, _ := os.ReadFile(filepath.Join(stateDir, "model.json"))
		var state map[string]any
		json.Unmarshal(stored, &state)
		recent, _ := state["recent"].([]any)

		count := 0
		for _, entry := range recent {
			e, _ := entry.(map[string]any)
			if e["modelID"] == "llama3.2" {
				count++
			}
		}
		if count != 1 {
			t.Errorf("expected 1 llama3.2 entry, got %d", count)
		}
	})

	t.Run("caps recent list at 10", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
		os.MkdirAll(stateDir, 0o755)

		// Pre-populate with 9 distinct ollama models
		recentEntries := make([]any, 0, 9)
		for i := range 9 {
			recentEntries = append(recentEntries, map[string]any{
				"providerID": "ollama",
				"modelID":    fmt.Sprintf("old-%d", i),
			})
		}
		initial := map[string]any{"recent": recentEntries}
		data, _ := json.MarshalIndent(initial, "", "  ")
		os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)

		// Add 5 new models — should cap at 10 total
		o := &OpenCode{}
		if err := o.Edit(testLaunchModels("new-0", "new-1", "new-2", "new-3", "new-4")); err != nil {
			t.Fatal(err)
		}

		stored, _ := os.ReadFile(filepath.Join(stateDir, "model.json"))
		var state map[string]any
		json.Unmarshal(stored, &state)
		recent, _ := state["recent"].([]any)

		if len(recent) != 10 {
			t.Errorf("expected 10 entries (capped), got %d", len(recent))
		}
	})
}

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

	// Default OLLAMA_HOST
	o.Edit(testLaunchModels("llama3.2"))

	var cfg map[string]any
	json.Unmarshal([]byte(o.configContent), &cfg)
	provider, _ := cfg["provider"].(map[string]any)
	ollama, _ := provider["ollama"].(map[string]any)
	options, _ := ollama["options"].(map[string]any)

	baseURL, _ := options["baseURL"].(string)
	if baseURL == "" {
		t.Error("baseURL should be set")
	}
}
