package launch

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

func assertKimiBinPath(t *testing.T, bin string) {
	t.Helper()
	base := strings.ToLower(filepath.Base(bin))
	if !strings.HasPrefix(base, "kimi") {
		t.Fatalf("bin = %q, want path to kimi executable", bin)
	}
}

func TestKimiIntegration(t *testing.T) {
	k := &Kimi{}

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

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

func TestKimiArgs(t *testing.T) {
	k := &Kimi{}

	got := k.args(`{"foo":"bar"}`, []string{"--quiet", "--print"})
	want := []string{"--config", `{"foo":"bar"}`, "--quiet", "--print"}
	if !slices.Equal(got, want) {
		t.Fatalf("args() = %v, want %v", got, want)
	}
}

func TestWindowsPathToWSL(t *testing.T) {
	tests := []struct {
		name  string
		in    string
		want  string
		valid bool
	}{
		{
			name:  "user profile path",
			in:    `C:\Users\parth`,
			want:  filepath.Join("/mnt", "c", "Users", "parth"),
			valid: true,
		},
		{
			name:  "path with trailing slash",
			in:    `D:\tools\bin\`,
			want:  filepath.Join("/mnt", "d", "tools", "bin"),
			valid: true,
		},
		{
			name:  "non windows path",
			in:    "/home/parth",
			valid: false,
		},
		{
			name:  "empty",
			in:    "",
			valid: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := windowsPathToWSL(tt.in)
			if !tt.valid {
				if got != "" {
					t.Fatalf("windowsPathToWSL(%q) = %q, want empty", tt.in, got)
				}
				return
			}
			if got != tt.want {
				t.Fatalf("windowsPathToWSL(%q) = %q, want %q", tt.in, got, tt.want)
			}
		})
	}
}

func TestFindKimiBinaryFallbacks(t *testing.T) {
	oldGOOS := kimiGOOS
	t.Cleanup(func() { kimiGOOS = oldGOOS })

	t.Run("linux/ubuntu uv tool path", func(t *testing.T) {
		homeDir := t.TempDir()
		setTestHome(t, homeDir)
		t.Setenv("PATH", t.TempDir())
		kimiGOOS = "linux"

		target := filepath.Join(homeDir, ".local", "share", "uv", "tools", "kimi-cli", "bin", "kimi")
		if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
			t.Fatalf("failed to create candidate dir: %v", err)
		}
		if err := os.WriteFile(target, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
			t.Fatalf("failed to write kimi candidate: %v", err)
		}

		got, err := findKimiBinary()
		if err != nil {
			t.Fatalf("findKimiBinary() error = %v", err)
		}
		if got != target {
			t.Fatalf("findKimiBinary() = %q, want %q", got, target)
		}
	})

	t.Run("windows appdata uv bin", func(t *testing.T) {
		setTestHome(t, t.TempDir())
		t.Setenv("PATH", t.TempDir())
		kimiGOOS = "windows"

		appDataDir := t.TempDir()
		t.Setenv("APPDATA", appDataDir)
		t.Setenv("LOCALAPPDATA", "")

		target := filepath.Join(appDataDir, "uv", "bin", "kimi.cmd")
		if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
			t.Fatalf("failed to create candidate dir: %v", err)
		}
		if err := os.WriteFile(target, []byte("@echo off\r\nexit /b 0\r\n"), 0o755); err != nil {
			t.Fatalf("failed to write kimi candidate: %v", err)
		}

		got, err := findKimiBinary()
		if err != nil {
			t.Fatalf("findKimiBinary() error = %v", err)
		}
		if got != target {
			t.Fatalf("findKimiBinary() = %q, want %q", got, target)
		}
	})
}

func TestValidateKimiPassthroughArgs_RejectsConflicts(t *testing.T) {
	tests := []struct {
		name string
		args []string
		want string
	}{
		{name: "--config", args: []string{"--config", "{}"}, want: "--config"},
		{name: "--config=", args: []string{"--config={}"}, want: "--config={"},
		{name: "--config-file", args: []string{"--config-file", "x.toml"}, want: "--config-file"},
		{name: "--config-file=", args: []string{"--config-file=x.toml"}, want: "--config-file=x.toml"},
		{name: "--model", args: []string{"--model", "foo"}, want: "--model"},
		{name: "--model=", args: []string{"--model=foo"}, want: "--model=foo"},
		{name: "-m", args: []string{"-m", "foo"}, want: "-m"},
		{name: "-m=", args: []string{"-m=foo"}, want: "-m=foo"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateKimiPassthroughArgs(tt.args)
			if err == nil {
				t.Fatalf("expected error for args %v", tt.args)
			}
			if !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("error %q does not contain %q", err.Error(), tt.want)
			}
		})
	}
}

func TestBuildKimiInlineConfig(t *testing.T) {
	t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")

	cfg, err := buildKimiInlineConfig("llama3.2", 65536)
	if err != nil {
		t.Fatalf("buildKimiInlineConfig() error = %v", err)
	}

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

	if parsed["default_model"] != "ollama" {
		t.Fatalf("default_model = %v, want ollama", parsed["default_model"])
	}

	providers, ok := parsed["providers"].(map[string]any)
	if !ok {
		t.Fatalf("providers missing or wrong type: %T", parsed["providers"])
	}
	ollamaProvider, ok := providers["ollama"].(map[string]any)
	if !ok {
		t.Fatalf("providers.ollama missing or wrong type: %T", providers["ollama"])
	}
	if ollamaProvider["type"] != "openai_legacy" {
		t.Fatalf("provider type = %v, want openai_legacy", ollamaProvider["type"])
	}
	if ollamaProvider["base_url"] != "http://127.0.0.1:11434/v1" {
		t.Fatalf("provider base_url = %v, want http://127.0.0.1:11434/v1", ollamaProvider["base_url"])
	}
	if ollamaProvider["api_key"] != "ollama" {
		t.Fatalf("provider api_key = %v, want ollama", ollamaProvider["api_key"])
	}

	models, ok := parsed["models"].(map[string]any)
	if !ok {
		t.Fatalf("models missing or wrong type: %T", parsed["models"])
	}
	ollamaModel, ok := models["ollama"].(map[string]any)
	if !ok {
		t.Fatalf("models.ollama missing or wrong type: %T", models["ollama"])
	}
	if ollamaModel["provider"] != "ollama" {
		t.Fatalf("model provider = %v, want ollama", ollamaModel["provider"])
	}
	if ollamaModel["model"] != "llama3.2" {
		t.Fatalf("model model = %v, want llama3.2", ollamaModel["model"])
	}
	if ollamaModel["max_context_size"] != float64(65536) {
		t.Fatalf("model max_context_size = %v, want 65536", ollamaModel["max_context_size"])
	}
}

func TestBuildKimiInlineConfig_UsesConnectableHostForUnspecifiedBind(t *testing.T) {
	t.Setenv("OLLAMA_HOST", "http://0.0.0.0:11434")

	cfg, err := buildKimiInlineConfig("llama3.2", 65536)
	if err != nil {
		t.Fatalf("buildKimiInlineConfig() error = %v", err)
	}

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

	providers, ok := parsed["providers"].(map[string]any)
	if !ok {
		t.Fatalf("providers missing or wrong type: %T", parsed["providers"])
	}

	ollamaProvider, ok := providers["ollama"].(map[string]any)
	if !ok {
		t.Fatalf("providers.ollama missing or wrong type: %T", providers["ollama"])
	}
	if got, _ := ollamaProvider["base_url"].(string); got != "http://127.0.0.1:11434/v1" {
		t.Fatalf("provider base_url = %q, want %q", got, "http://127.0.0.1:11434/v1")
	}
}

func TestResolveKimiMaxContextSize(t *testing.T) {
	t.Run("uses cloud limit when known", func(t *testing.T) {
		got := resolveKimiMaxContextSize("kimi-k2.5:cloud")
		if got != 262_144 {
			t.Fatalf("resolveKimiMaxContextSize() = %d, want 262144", got)
		}
	})

	t.Run("uses model show context length for local models", func(t *testing.T) {
		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if r.URL.Path != "/api/show" {
				http.NotFound(w, r)
				return
			}
			fmt.Fprint(w, `{"model_info":{"llama.context_length":131072}}`)
		}))
		defer srv.Close()
		t.Setenv("OLLAMA_HOST", srv.URL)

		got := resolveKimiMaxContextSize("llama3.2")
		if got != 131_072 {
			t.Fatalf("resolveKimiMaxContextSize() = %d, want 131072", got)
		}
	})

	t.Run("falls back to default when show fails", func(t *testing.T) {
		srv := httptest.NewServer(http.NotFoundHandler())
		defer srv.Close()
		t.Setenv("OLLAMA_HOST", srv.URL)

		oldTimeout := kimiModelShowTimeout
		kimiModelShowTimeout = 100 * 1000 * 1000 // 100ms
		t.Cleanup(func() { kimiModelShowTimeout = oldTimeout })

		got := resolveKimiMaxContextSize("llama3.2")
		if got != kimiDefaultMaxContextSize {
			t.Fatalf("resolveKimiMaxContextSize() = %d, want %d", got, kimiDefaultMaxContextSize)
		}
	})
}

func TestKimiRun_RejectsConflictingArgsBeforeInstall(t *testing.T) {
	k := &Kimi{}

	oldConfirm := DefaultConfirmPrompt
	DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
		t.Fatalf("did not expect install prompt, got %q", prompt)
		return false, nil
	}
	t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })

	err := k.Run("llama3.2", nil, []string{"--model", "other"})
	if err == nil || !strings.Contains(err.Error(), "--model") {
		t.Fatalf("expected conflict error mentioning --model, got %v", err)
	}
}

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

	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)
	logPath := filepath.Join(tmpDir, "kimi-args.log")
	script := fmt.Sprintf(`#!/bin/sh
for arg in "$@"; do
  printf "%%s\n" "$arg" >> %q
done
exit 0
`, logPath)
	if err := os.WriteFile(filepath.Join(tmpDir, "kimi"), []byte(script), 0o755); err != nil {
		t.Fatalf("failed to write fake kimi: %v", err)
	}
	t.Setenv("PATH", tmpDir)

	srv := httptest.NewServer(http.NotFoundHandler())
	defer srv.Close()
	t.Setenv("OLLAMA_HOST", srv.URL)

	k := &Kimi{}
	if err := k.Run("llama3.2", nil, []string{"--quiet", "--print"}); err != nil {
		t.Fatalf("Run() error = %v", err)
	}

	data, err := os.ReadFile(logPath)
	if err != nil {
		t.Fatalf("failed to read args log: %v", err)
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	if len(lines) < 4 {
		t.Fatalf("expected at least 4 args, got %v", lines)
	}
	if lines[0] != "--config" {
		t.Fatalf("first arg = %q, want --config", lines[0])
	}

	var cfg map[string]any
	if err := json.Unmarshal([]byte(lines[1]), &cfg); err != nil {
		t.Fatalf("config arg is not valid JSON: %v", err)
	}
	providers := cfg["providers"].(map[string]any)
	ollamaProvider := providers["ollama"].(map[string]any)
	if ollamaProvider["type"] != "openai_legacy" {
		t.Fatalf("provider type = %v, want openai_legacy", ollamaProvider["type"])
	}

	if lines[2] != "--quiet" || lines[3] != "--print" {
		t.Fatalf("extra args = %v, want [--quiet --print]", lines[2:])
	}
}

func TestEnsureKimiInstalled(t *testing.T) {
	oldGOOS := kimiGOOS
	t.Cleanup(func() { kimiGOOS = 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)
		writeFakeBinary(t, tmpDir, "kimi")
		kimiGOOS = runtime.GOOS

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

		bin, err := ensureKimiInstalled()
		if err != nil {
			t.Fatalf("ensureKimiInstalled() error = %v", err)
		}
		assertKimiBinPath(t, bin)
	})

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

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

		_, err := ensureKimiInstalled()
		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)
		writeFakeBinary(t, tmpDir, "curl")
		writeFakeBinary(t, tmpDir, "bash")
		kimiGOOS = "linux"

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

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

	t.Run("missing and user confirms install succeeds", 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)
		kimiGOOS = "linux"

		writeFakeBinary(t, tmpDir, "curl")

		installLog := filepath.Join(tmpDir, "bash.log")
		kimiPath := filepath.Join(tmpDir, "kimi")
		bashScript := fmt.Sprintf(`#!/bin/sh
echo "$@" >> %q
if [ "$1" = "-c" ]; then
  /bin/cat > %q <<'EOS'
#!/bin/sh
exit 0
EOS
  /bin/chmod +x %q
fi
exit 0
`, installLog, kimiPath, kimiPath)
		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 := ensureKimiInstalled()
		if err != nil {
			t.Fatalf("ensureKimiInstalled() error = %v", err)
		}
		assertKimiBinPath(t, bin)

		logData, err := os.ReadFile(installLog)
		if err != nil {
			t.Fatalf("failed to read install log: %v", err)
		}
		if !strings.Contains(string(logData), "https://code.kimi.com/install.sh") {
			t.Fatalf("expected install.sh command in log, got:\n%s", string(logData))
		}
	})

	t.Run("install succeeds and kimi is in home local bin without PATH update", func(t *testing.T) {
		if runtime.GOOS == "windows" {
			t.Skip("uses POSIX shell fake binaries")
		}

		homeDir := t.TempDir()
		setTestHome(t, homeDir)

		tmpBin := t.TempDir()
		t.Setenv("PATH", tmpBin)
		kimiGOOS = "linux"
		writeFakeBinary(t, tmpBin, "curl")

		installedKimi := filepath.Join(homeDir, ".local", "bin", "kimi")
		bashScript := fmt.Sprintf(`#!/bin/sh
if [ "$1" = "-c" ]; then
  /bin/mkdir -p %q
  /bin/cat > %q <<'EOS'
#!/bin/sh
exit 0
EOS
  /bin/chmod +x %q
fi
exit 0
`, filepath.Dir(installedKimi), installedKimi, installedKimi)
		if err := os.WriteFile(filepath.Join(tmpBin, "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 := ensureKimiInstalled()
		if err != nil {
			t.Fatalf("ensureKimiInstalled() error = %v", err)
		}
		if bin != installedKimi {
			t.Fatalf("bin = %q, want %q", bin, installedKimi)
		}
	})

	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)
		kimiGOOS = "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 := ensureKimiInstalled()
		if err == nil || !strings.Contains(err.Error(), "failed to install kimi") {
			t.Fatalf("expected install failure error, got %v", err)
		}
	})

	t.Run("install succeeds but binary missing on PATH", 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)
		kimiGOOS = "linux"
		writeFakeBinary(t, tmpDir, "curl")
		if err := os.WriteFile(filepath.Join(tmpDir, "bash"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
			t.Fatalf("failed to write fake bash: %v", err)
		}

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

		_, err := ensureKimiInstalled()
		if err == nil || !strings.Contains(err.Error(), "binary was not found on PATH") {
			t.Fatalf("expected PATH guidance error, got %v", err)
		}
	})
}

func TestKimiInstallerCommand(t *testing.T) {
	tests := []struct {
		name      string
		goos      string
		wantBin   string
		wantParts []string
		wantErr   bool
	}{
		{
			name:      "linux",
			goos:      "linux",
			wantBin:   "bash",
			wantParts: []string{"-c", "install.sh"},
		},
		{
			name:      "darwin",
			goos:      "darwin",
			wantBin:   "bash",
			wantParts: []string{"-c", "install.sh"},
		},
		{
			name:      "windows",
			goos:      "windows",
			wantBin:   "powershell",
			wantParts: []string{"-Command", "install.ps1"},
		},
		{
			name:    "unsupported",
			goos:    "freebsd",
			wantErr: true,
		},
	}

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