package profile

import (
	"os"
	"path/filepath"
	"runtime"
	"slices"
	"testing"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
)

func TestRuntimeManager_Get(t *testing.T) {
	t.Parallel()

	mgr := &Manager{
		runtimes: map[string]*runtimeconfig.Runtime{
			"opencode": {Type: runtimeconfig.TypeOpenCode, Name: "opencode", Binary: "opencode"},
			"claude":   {Type: runtimeconfig.TypeClaudeCode, Name: "claude", Binary: "claude"},
		},
	}
	rm := &RuntimeManager{mgr: mgr}

	t.Run("found", func(t *testing.T) {
		rt, ok := rm.Get("opencode")
		if !ok {
			t.Fatal("expected to find opencode")
		}
		if rt.Type != runtimeconfig.TypeOpenCode {
			t.Errorf("expected TypeOpenCode, got %v", rt.Type)
		}
	})

	t.Run("not found", func(t *testing.T) {
		_, ok := rm.Get("nonexistent")
		if ok {
			t.Error("expected not to find nonexistent")
		}
	})
}

func TestRuntimeManagerRefreshRuntimeWatchesAllowsMissingWatcher(t *testing.T) {
	t.Parallel()

	mgr := &Manager{
		runtimes: map[string]*runtimeconfig.Runtime{
			"opencode": {Type: runtimeconfig.TypeOpenCode, Name: "opencode", Binary: "opencode"},
		},
	}
	rm := &RuntimeManager{mgr: mgr}

	rm.refreshRuntimeWatches()
}

func TestRuntimeManager_Detect(t *testing.T) {
	t.Parallel()

	mgr := &Manager{}
	rm := &RuntimeManager{mgr: mgr}
	rm.detectRuntimes()

	results := rm.Detect()
	if len(results) == 0 {
		t.Fatal("expected at least one runtime type to be checked")
	}

	seen := make(map[string]bool)
	for _, r := range results {
		if seen[string(r.Type)] {
			t.Errorf("duplicate runtime type %q", r.Type)
		}
		seen[string(r.Type)] = true
		if r.Binary == "" {
			t.Errorf("empty binary for type %q", r.Type)
		}
	}

	for _, rt := range runtimeconfig.AllRuntimeTypes {
		if !seen[string(rt)] {
			t.Errorf("missing detection for runtime type %q", rt)
		}
	}
}

func TestRuntimeManager_RefreshDetectForcesRescanWithSamePATH(t *testing.T) {
	originalScan := runtimeDetectionScan
	var calls []bool
	runtimeDetectionScan = func(force bool) ([]runtimeconfig.DetectResult, map[string]*runtimeconfig.Runtime) {
		calls = append(calls, force)
		result := runtimeconfig.DetectResult{
			Type:    runtimeconfig.TypeClaudeCode,
			Binary:  "claude",
			Path:    "/mock/bin/claude",
			Found:   force,
			Version: "claude-test",
		}
		runtimes := make(map[string]*runtimeconfig.Runtime)
		if force {
			runtimes["claude"] = &runtimeconfig.Runtime{
				Type:   runtimeconfig.TypeClaudeCode,
				Name:   "claude",
				Binary: "claude",
			}
		}
		return []runtimeconfig.DetectResult{result}, runtimes
	}
	t.Cleanup(func() { runtimeDetectionScan = originalScan })

	mgr := &Manager{runtimes: make(map[string]*runtimeconfig.Runtime)}
	rm := &RuntimeManager{mgr: mgr}
	rm.runDetect()

	if result := findRuntimeDetectResult(t, rm.Detect(), runtimeconfig.TypeClaudeCode); result.Found {
		t.Fatalf("claude Found before binary exists = true")
	}

	results := rm.RefreshDetect()
	result := findRuntimeDetectResult(t, results, runtimeconfig.TypeClaudeCode)
	if !result.Found {
		t.Fatalf("claude Found after RefreshDetect = false")
	}
	if result.Path != "/mock/bin/claude" {
		t.Fatalf("claude Path = %q, want /mock/bin/claude", result.Path)
	}
	if result.Version != "claude-test" {
		t.Fatalf("claude Version = %q, want claude-test", result.Version)
	}
	if _, ok := rm.Get("claude"); !ok {
		t.Fatal("claude runtime was not registered after RefreshDetect")
	}
	if len(calls) != 2 || calls[0] || !calls[1] {
		t.Fatalf("runtimeDetectionScan force calls = %v, want [false true]", calls)
	}
}

func TestRuntimeManager_GetOrRefreshRetriesAfterRuntimeMiss(t *testing.T) {
	originalScan := runtimeDetectionScan
	calls := 0
	runtimeDetectionScan = func(force bool) ([]runtimeconfig.DetectResult, map[string]*runtimeconfig.Runtime) {
		calls++
		if !force {
			t.Fatalf("GetOrRefresh scan force = false")
		}
		return []runtimeconfig.DetectResult{{
				Type:      runtimeconfig.TypeCodex,
				Binary:    "codex",
				Path:      "/mock/bin/codex",
				Found:     true,
				Installed: true,
				Capable:   true,
			}},
			map[string]*runtimeconfig.Runtime{
				"codex": {
					Type:   runtimeconfig.TypeCodex,
					Name:   "codex",
					Binary: "codex",
				},
			}
	}
	t.Cleanup(func() { runtimeDetectionScan = originalScan })

	mgr := &Manager{runtimes: make(map[string]*runtimeconfig.Runtime)}
	rm := &RuntimeManager{mgr: mgr}

	rt, ok := rm.GetOrRefresh("codex")
	if !ok {
		t.Fatal("codex runtime was not registered after refresh retry")
	}
	if rt.Name != "codex" {
		t.Fatalf("runtime name = %q, want codex", rt.Name)
	}
	if calls != 1 {
		t.Fatalf("runtimeDetectionScan calls = %d, want 1", calls)
	}
}

func TestRuntimeManagerOnDetectChangeNotifiesCapabilitySetChanges(t *testing.T) {
	originalScan := runtimeDetectionScan
	calls := 0
	runtimeDetectionScan = func(force bool) ([]runtimeconfig.DetectResult, map[string]*runtimeconfig.Runtime) {
		calls++
		runtimes := map[string]*runtimeconfig.Runtime{
			"claude": {Type: runtimeconfig.TypeClaudeCode, Name: "claude", Binary: "claude"},
		}
		results := []runtimeconfig.DetectResult{{Type: runtimeconfig.TypeClaudeCode, Found: true, Capable: true}}
		if calls >= 3 {
			runtimes["codex"] = &runtimeconfig.Runtime{Type: runtimeconfig.TypeCodex, Name: "codex", Binary: "codex"}
			results = append(results, runtimeconfig.DetectResult{Type: runtimeconfig.TypeCodex, Found: true, Capable: true})
		}
		return results, runtimes
	}
	t.Cleanup(func() { runtimeDetectionScan = originalScan })

	mgr := &Manager{runtimes: make(map[string]*runtimeconfig.Runtime)}
	rm := &RuntimeManager{mgr: mgr}

	var changes []RuntimeDetectChange
	unsubscribe := rm.OnDetectChange(func(change RuntimeDetectChange) {
		changes = append(changes, change)
	})
	defer unsubscribe()

	rm.runDetect()
	rm.RefreshDetect()
	if len(changes) != 0 {
		t.Fatalf("changes after unchanged refresh = %d, want 0", len(changes))
	}

	rm.RefreshDetect()
	if len(changes) != 1 {
		t.Fatalf("changes after capability refresh = %d, want 1", len(changes))
	}
	if got, want := changes[0].Previous, []string{"claude"}; !slices.Equal(got, want) {
		t.Fatalf("previous runtimes = %v, want %v", got, want)
	}
	if got, want := changes[0].Current, []string{"claude", "codex"}; !slices.Equal(got, want) {
		t.Fatalf("current runtimes = %v, want %v", got, want)
	}

	unsubscribe()
	rm.RefreshDetect()
	if len(changes) != 1 {
		t.Fatalf("changes after unsubscribe = %d, want 1", len(changes))
	}
}

func TestRuntimeManager_All(t *testing.T) {
	t.Parallel()

	mgr := &Manager{
		runtimes: map[string]*runtimeconfig.Runtime{
			"claude":   {Type: runtimeconfig.TypeClaudeCode, Name: "claude", Binary: "claude"},
			"opencode": {Type: runtimeconfig.TypeOpenCode, Name: "opencode", Binary: "opencode"},
		},
	}
	rm := &RuntimeManager{mgr: mgr}

	all := rm.All()
	if len(all) != 2 {
		t.Fatalf("expected 2 runtimes, got %d", len(all))
	}

	for _, rt := range all {
		if rt.Type != runtimeconfig.TypeClaudeCode && rt.Type != runtimeconfig.TypeOpenCode {
			t.Errorf("unexpected runtime type %v", rt.Type)
		}
	}
}

func findRuntimeDetectResult(t *testing.T, results []runtimeconfig.DetectResult, runtimeType runtimeconfig.RuntimeType) runtimeconfig.DetectResult {
	t.Helper()
	for _, result := range results {
		if result.Type == runtimeType {
			return result
		}
	}
	t.Fatalf("detect result for %s not found", runtimeType)
	return runtimeconfig.DetectResult{}
}

func TestRuntimeManager_All_Empty(t *testing.T) {
	t.Parallel()

	mgr := &Manager{runtimes: make(map[string]*runtimeconfig.Runtime)}
	rm := &RuntimeManager{mgr: mgr}

	all := rm.All()
	if len(all) != 0 {
		t.Errorf("expected 0 runtimes, got %d", len(all))
	}
}

func TestRuntimeManager_StartStopDetect(t *testing.T) {
	originalScan := runtimeDetectionScan
	scanCalls := 0
	runtimeDetectionScan = func(force bool) ([]runtimeconfig.DetectResult, map[string]*runtimeconfig.Runtime) {
		scanCalls++
		return []runtimeconfig.DetectResult{{
				Type:  runtimeconfig.TypeCodex,
				Found: true,
			}},
			map[string]*runtimeconfig.Runtime{
				"codex": {
					Type:   runtimeconfig.TypeCodex,
					Name:   "codex",
					Binary: "codex",
				},
			}
	}
	t.Cleanup(func() { runtimeDetectionScan = originalScan })

	mgr := &Manager{runtimes: make(map[string]*runtimeconfig.Runtime)}
	rm := &RuntimeManager{mgr: mgr}

	t.Run("stop when not started is no-op", func(t *testing.T) {
		rm.StopDetect()
	})

	t.Run("start begins detection", func(t *testing.T) {
		rm.StartDetect()
		if scanCalls != 1 {
			t.Fatalf("StartDetect scan calls = %d, want 1", scanCalls)
		}
		if _, ok := rm.Get("codex"); !ok {
			t.Fatal("StartDetect did not register detected codex runtime")
		}
		rm.StopDetect()
	})

	t.Run("double start is idempotent", func(t *testing.T) {
		rm.StartDetect()
		rm.StartDetect()
		if scanCalls != 2 {
			t.Fatalf("StartDetect scan calls = %d, want 2 after second start cycle", scanCalls)
		}
		rm.StopDetect()
	})
}

func TestRuntimeManager_DetectRuntimes(t *testing.T) {
	t.Parallel()

	tmpDir := t.TempDir()
	mgr := &Manager{
		runtimes: make(map[string]*runtimeconfig.Runtime),
	}

	binPath := filepath.Join(tmpDir, "test-runtime")
	if err := os.WriteFile(binPath, []byte("#!/bin/sh\n"), 0755); err != nil {
		t.Fatal(err)
	}

	rm := &RuntimeManager{mgr: mgr}
	rm.detectRuntimes()

	_ = binPath
}

func TestRunDetectReportsCodexWithoutAppServerButDoesNotRegisterCandidate(t *testing.T) {
	tmpDir := t.TempDir()
	codexPath := filepath.Join(tmpDir, "codex")
	if err := os.WriteFile(codexPath, []byte("#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo codex-test; fi\n"), 0755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", tmpDir)

	oldProbe := runtimeconfig.CodexAppServerAvailable
	t.Cleanup(func() { runtimeconfig.CodexAppServerAvailable = oldProbe })
	runtimeconfig.CodexAppServerAvailable = func(string) bool { return false }

	mgr := &Manager{runtimes: make(map[string]*runtimeconfig.Runtime)}
	rm := &RuntimeManager{mgr: mgr}
	rm.runDetect()

	codexResult := findRuntimeDetectResult(t, mgr.cachedDetectResults, runtimeconfig.TypeCodex)
	if codexResult.Path != codexPath {
		t.Fatalf("codex Path = %q, want %q", codexResult.Path, codexPath)
	}
	if codexResult.Found || !codexResult.Installed || codexResult.Capable {
		t.Fatalf("codex detection = found:%v installed:%v capable:%v, want found:false installed:true capable:false", codexResult.Found, codexResult.Installed, codexResult.Capable)
	}
	if codexResult.ReasonCode != runtimeconfig.RuntimeProbeReasonCapabilityMissing {
		t.Fatalf("codex ReasonCode = %q, want %q", codexResult.ReasonCode, runtimeconfig.RuntimeProbeReasonCapabilityMissing)
	}
	if _, ok := rm.Get("codex"); !ok {
		return
	}
	t.Fatal("codex runtime was registered despite missing app-server capability")
}

func TestDetectRuntimeVersionUsesShellPath(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("test uses POSIX shell scripts")
	}
	runtimeconfig.SetShellPathForRuntimeResolution("")
	runtimeconfig.SetShellEnvForRuntimeResolution(nil)
	t.Cleanup(func() {
		runtimeconfig.SetShellPathForRuntimeResolution("")
		runtimeconfig.SetShellEnvForRuntimeResolution(nil)
	})

	runtimeDir := t.TempDir()
	helperDir := t.TempDir()
	codexPath := filepath.Join(runtimeDir, "codex")
	if err := os.WriteFile(codexPath, []byte("#!/bin/sh\nversion-helper\n"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(helperDir, "version-helper"), []byte("#!/bin/sh\necho codex-shell-version\n"), 0o755); err != nil {
		t.Fatal(err)
	}

	t.Setenv("PATH", runtimeDir)
	runtimeconfig.SetShellPathForRuntimeResolution(helperDir)

	version := detectRuntimeVersion(runtimeconfig.TypeCodex, codexPath)
	if version != "codex-shell-version" {
		t.Fatalf("detectRuntimeVersion() = %q, want codex-shell-version", version)
	}
}

func TestDetectRuntimeVersionSupportsKimi(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("test uses POSIX shell scripts")
	}
	runtimeDir := t.TempDir()
	kimiPath := filepath.Join(runtimeDir, "kimi")
	if err := os.WriteFile(kimiPath, []byte("#!/bin/sh\necho kimi-version\n"), 0o755); err != nil {
		t.Fatal(err)
	}

	version := detectRuntimeVersion(runtimeconfig.TypeKimi, kimiPath)
	if version != "kimi-version" {
		t.Fatalf("detectRuntimeVersion() = %q, want kimi-version", version)
	}
}

func TestRuntimeManager_SortedRuntimes(t *testing.T) {
	t.Parallel()

	mgr := &Manager{
		runtimes: map[string]*runtimeconfig.Runtime{
			"opencode": {Type: runtimeconfig.TypeOpenCode, Name: "opencode"},
			"claude":   {Type: runtimeconfig.TypeClaudeCode, Name: "claude"},
		},
	}
	rm := &RuntimeManager{mgr: mgr}

	sorted := rm.All()
	if len(sorted) != 2 {
		t.Fatalf("expected 2 runtimes, got %d", len(sorted))
	}

	for i := 1; i < len(sorted); i++ {
		prev := runtimeconfig.RuntimePriority(sorted[i-1].Type)
		curr := runtimeconfig.RuntimePriority(sorted[i].Type)
		if prev > curr {
			t.Errorf("not sorted: %v (priority %d) before %v (priority %d)",
				sorted[i-1].Type, prev, sorted[i].Type, curr)
		}
	}
}
