package runtimeconfig

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

	"github.com/stretchr/testify/require"
)

func TestPrepareManagedOpenCodeEnvCreatesPersistentPrivateXDGDirectories(t *testing.T) {
	runtimeStateDir := filepath.Join(t.TempDir(), "agents", "agt_one", ".runtime")
	modelsPath := filepath.Join(t.TempDir(), "models.json")
	require.NoError(t, os.MkdirAll(filepath.Join(runtimeStateDir, "opencode", "data"), 0o755))

	first, err := PrepareManagedOpenCodeEnv(modelsPath, runtimeStateDir)
	require.NoError(t, err)
	second, err := PrepareManagedOpenCodeEnv(modelsPath, runtimeStateDir)
	require.NoError(t, err)
	require.Equal(t, first, second)
	other, err := PrepareManagedOpenCodeEnv(modelsPath, filepath.Join(t.TempDir(), "agents", "agt_two", ".runtime"))
	require.NoError(t, err)
	require.NotEqual(t, first, other)

	require.Equal(t, "1", first["OPENCODE_DISABLE_MODELS_FETCH"])
	require.Equal(t, "1", first["OPENCODE_DISABLE_LSP_DOWNLOAD"])
	require.Equal(t, "1", first["OPENCODE_DISABLE_AUTOUPDATE"])
	require.Equal(t, "1", first["OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"])
	require.Equal(t, modelsPath, first["OPENCODE_MODELS_PATH"])
	require.NotContains(t, first, "OPENCODE_DISABLE_PROJECT_CONFIG")
	require.NotContains(t, first, "OPENCODE_DISABLE_DEFAULT_PLUGINS")
	require.NotContains(t, first, "OPENCODE_DISABLE_EXTERNAL_SKILLS")
	require.NotContains(t, first, "OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
	require.NotContains(t, first, "OPENCODE_TEST_HOME")
	require.NotContains(t, first, "XDG_CONFIG_HOME")

	root := filepath.Join(runtimeStateDir, "opencode")
	require.Equal(t, filepath.Join(root, "data"), first["XDG_DATA_HOME"])
	require.Equal(t, filepath.Join(root, "state"), first["XDG_STATE_HOME"])
	require.Equal(t, filepath.Join(root, "cache"), first["XDG_CACHE_HOME"])
	for _, key := range []string{"XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"} {
		dir := first[key]
		info, statErr := os.Stat(dir)
		require.NoError(t, statErr)
		require.True(t, info.IsDir())
		if runtime.GOOS != "windows" {
			require.Equal(t, os.FileMode(0o700), info.Mode().Perm())
		}
	}
}

func TestProbeManagedOpenCodeReportsMissingManagedRuntime(t *testing.T) {
	t.Setenv(managedOpenCodePathEnv, filepath.Join(t.TempDir(), "missing-opencode"))
	setManagedOpenCodeVersion(t, "")

	probe := ProbeManagedOpenCode()

	require.False(t, probe.Installed)
	require.False(t, probe.Capable)
	require.Equal(t, RuntimeProbeReasonAgentTeamInstallIncomplete, probe.ReasonCode)
	require.Contains(t, probe.Message, "AgentTeam Desktop installation incomplete")
	require.Error(t, probe.Err)
}

func TestProbeManagedOpenCodeReportsNonExecutableRuntime(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("Windows executable probing does not use Unix permission bits")
	}
	opencodePath := writeManagedOpenCodeBinary(t, "opencode 1.2.3\n", 0o644)
	writeManagedOpenCodeModels(t, opencodePath)
	t.Setenv(managedOpenCodePathEnv, opencodePath)
	setManagedOpenCodeVersion(t, "")

	probe := ProbeManagedOpenCode()

	require.Equal(t, opencodePath, probe.Path)
	require.True(t, probe.Installed)
	require.False(t, probe.Capable)
	require.Equal(t, RuntimeProbeReasonAgentTeamInstallIncomplete, probe.ReasonCode)
	require.Contains(t, probe.Message, "not executable")
	require.Error(t, probe.Err)
}

func TestProbeManagedOpenCodeReportsMissingModelsCatalog(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("test uses POSIX shell scripts")
	}
	opencodePath := writeManagedOpenCodeBinary(t, "opencode 1.2.3\n", 0o755)
	t.Setenv(managedOpenCodePathEnv, opencodePath)
	setManagedOpenCodeVersion(t, "")

	probe := ProbeManagedOpenCode()

	require.Equal(t, opencodePath, probe.Path)
	require.True(t, probe.Installed)
	require.False(t, probe.Capable)
	require.Equal(t, RuntimeProbeReasonAgentTeamInstallIncomplete, probe.ReasonCode)
	require.Contains(t, probe.Message, "models catalog")
	require.Contains(t, probe.Message, "missing")
	require.Error(t, probe.Err)
}

func TestProbeManagedOpenCodeReportsDirectoryModelsCatalog(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("test uses POSIX shell scripts")
	}
	opencodePath := writeManagedOpenCodeBinary(t, "opencode 1.2.3\n", 0o755)
	require.NoError(t, os.Mkdir(filepath.Join(filepath.Dir(opencodePath), "models.json"), 0o755))
	t.Setenv(managedOpenCodePathEnv, opencodePath)
	setManagedOpenCodeVersion(t, "")

	probe := ProbeManagedOpenCode()

	require.Equal(t, opencodePath, probe.Path)
	require.True(t, probe.Installed)
	require.False(t, probe.Capable)
	require.Equal(t, RuntimeProbeReasonAgentTeamInstallIncomplete, probe.ReasonCode)
	require.Contains(t, probe.Message, "models catalog path")
	require.Contains(t, probe.Message, "not a regular file")
	require.Error(t, probe.Err)
}

func TestProbeManagedOpenCodeReportsVersionProbeFailureWithoutExpectedVersion(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("test uses POSIX shell scripts")
	}
	opencodePath := writeBrokenManagedOpenCodeBinary(t)
	writeManagedOpenCodeModels(t, opencodePath)
	t.Setenv(managedOpenCodePathEnv, opencodePath)
	setManagedOpenCodeVersion(t, "")

	probe := ProbeManagedOpenCode()

	require.Equal(t, opencodePath, probe.Path)
	require.True(t, probe.Installed)
	require.False(t, probe.Capable)
	require.Equal(t, RuntimeProbeReasonAgentTeamInstallIncomplete, probe.ReasonCode)
	require.Contains(t, probe.Message, "AgentTeam Desktop installation incomplete")
	require.Contains(t, probe.Message, "cannot report its version")
	require.NotContains(t, probe.Message, "opencode-darwin-arm64")
	require.Error(t, probe.Err)
}

func TestProbeManagedOpenCodeReportsVersionMismatch(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("test uses POSIX shell scripts")
	}
	opencodePath := writeManagedOpenCodeBinary(t, "opencode 1.2.3\n", 0o755)
	writeManagedOpenCodeModels(t, opencodePath)
	t.Setenv(managedOpenCodePathEnv, opencodePath)
	setManagedOpenCodeVersion(t, "1.2.4")

	probe := ProbeManagedOpenCode()

	require.Equal(t, opencodePath, probe.Path)
	require.True(t, probe.Installed)
	require.False(t, probe.Capable)
	require.Equal(t, RuntimeProbeReasonVersionMismatch, probe.ReasonCode)
	require.Contains(t, probe.Message, "does not match expected")
	require.Error(t, probe.Err)
}

func TestManagedOpenCodeCandidatePathsElectronResourcesLayoutIsExclusive(t *testing.T) {
	root := t.TempDir()
	resourcesDir := filepath.Join(root, "resources")
	executable := filepath.Join(resourcesDir, "bin", managedAgentTeamBinaryName(runtime.GOOS))
	setManagedOpenCodeExecutable(t, executable)
	t.Setenv(managedOpenCodePathEnv, "")

	require.Equal(t, []string{
		filepath.Join(resourcesDir, "runtimes", "opencode", managedOpenCodeBinaryName(runtime.GOOS)),
	}, ManagedOpenCodeCandidatePaths())
}

func TestProbeManagedOpenCodeElectronResourcesLayoutDoesNotFallBackAdjacent(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("test uses POSIX shell scripts")
	}
	root := t.TempDir()
	resourcesDir := filepath.Join(root, "resources")
	binDir := filepath.Join(resourcesDir, "bin")
	require.NoError(t, os.MkdirAll(binDir, 0o755))
	executable := filepath.Join(binDir, managedAgentTeamBinaryName(runtime.GOOS))
	require.NoError(t, os.WriteFile(executable, []byte("agentteam"), 0o755))
	setManagedOpenCodeExecutable(t, executable)
	t.Setenv(managedOpenCodePathEnv, "")
	setManagedOpenCodeVersion(t, "1.2.3")

	adjacent := filepath.Join(binDir, managedOpenCodeBinaryName(runtime.GOOS))
	writeManagedOpenCodeBinaryAt(t, adjacent, "opencode 1.2.3\n", 0o755)
	writeManagedOpenCodeModels(t, adjacent)

	probe := ProbeManagedOpenCode()
	require.False(t, probe.Installed)
	require.False(t, probe.Capable)
	require.Equal(t, RuntimeProbeReasonAgentTeamInstallIncomplete, probe.ReasonCode)
	require.NotEqual(t, adjacent, probe.Path)
}

func TestProbeManagedOpenCodeElectronResourcesLayoutRequiresVersionedPayload(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("test uses POSIX shell scripts")
	}
	root := t.TempDir()
	resourcesDir := filepath.Join(root, "resources")
	executable := filepath.Join(resourcesDir, "bin", managedAgentTeamBinaryName(runtime.GOOS))
	require.NoError(t, os.MkdirAll(filepath.Dir(executable), 0o755))
	require.NoError(t, os.WriteFile(executable, []byte("agentteam"), 0o755))
	setManagedOpenCodeExecutable(t, executable)
	t.Setenv(managedOpenCodePathEnv, "")
	setManagedOpenCodeVersion(t, "")

	runtimePath := filepath.Join(
		resourcesDir,
		"runtimes",
		"opencode",
		managedOpenCodeBinaryName(runtime.GOOS),
	)
	writeManagedOpenCodeBinaryAt(t, runtimePath, "opencode 1.2.3\n", 0o755)
	writeManagedOpenCodeModels(t, runtimePath)

	probe := ProbeManagedOpenCode()
	require.True(t, probe.Installed)
	require.False(t, probe.Capable)
	require.Equal(t, RuntimeProbeReasonAgentTeamInstallIncomplete, probe.ReasonCode)
	require.Contains(t, probe.Message, "version")
	require.Contains(t, probe.Message, "not embedded")
}

func TestManagedOpenCodeCandidatePathsOrdinaryCLIKeepsAdjacentCandidates(t *testing.T) {
	root := t.TempDir()
	executable := filepath.Join(root, "bin", managedAgentTeamBinaryName(runtime.GOOS))
	setManagedOpenCodeExecutable(t, executable)
	t.Setenv(managedOpenCodePathEnv, "")

	binary := managedOpenCodeBinaryName(runtime.GOOS)
	require.Equal(t, []string{
		filepath.Join(root, "bin", "runtimes", "opencode", binary),
		filepath.Join(root, "bin", "runtimes", binary),
		filepath.Join(root, "bin", binary),
	}, ManagedOpenCodeCandidatePaths())
}

func TestManagedOpenCodeOverrideRemainsManualPathForOrdinaryCLI(t *testing.T) {
	external := filepath.Join(t.TempDir(), "manual", managedOpenCodeBinaryName(runtime.GOOS))
	t.Setenv(managedOpenCodePathEnv, external)
	setManagedOpenCodeExecutable(t, filepath.Join(t.TempDir(), "bin", managedAgentTeamBinaryName(runtime.GOOS)))

	require.Equal(t, []string{external}, ManagedOpenCodeCandidatePaths())
}

func TestManagedOpenCodeElectronResourcesLayoutExcludesManualOverride(t *testing.T) {
	root := t.TempDir()
	resourcesDir := filepath.Join(root, "resources")
	external := filepath.Join(t.TempDir(), "user", ".local", "bin", managedOpenCodeBinaryName(runtime.GOOS))
	t.Setenv(managedOpenCodePathEnv, external)
	setManagedOpenCodeExecutable(t, filepath.Join(resourcesDir, "bin", managedAgentTeamBinaryName(runtime.GOOS)))

	require.Equal(t, []string{
		filepath.Join(resourcesDir, "runtimes", "opencode", managedOpenCodeBinaryName(runtime.GOOS)),
	}, ManagedOpenCodeCandidatePaths())
}

func writeManagedOpenCodeBinary(t *testing.T, versionOutput string, mode os.FileMode) string {
	t.Helper()
	name := managedOpenCodeBinaryName(runtime.GOOS)
	path := filepath.Join(t.TempDir(), name)
	writeManagedOpenCodeBinaryAt(t, path, versionOutput, mode)
	return path
}

func writeManagedOpenCodeBinaryAt(t *testing.T, path, versionOutput string, mode os.FileMode) {
	t.Helper()
	script := "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then\ncat <<'EOF'\n" + versionOutput + "EOF\nfi\nexit 0\n"
	require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
	require.NoError(t, os.WriteFile(path, []byte(script), mode))
}

func writeManagedOpenCodeModels(t *testing.T, opencodePath string) string {
	t.Helper()
	path := filepath.Join(filepath.Dir(opencodePath), "models.json")
	require.NoError(t, os.WriteFile(path, []byte(`{"local":{"id":"local","name":"Local","env":[],"models":{}}}`+"\n"), 0o644))
	return path
}

func writeBrokenManagedOpenCodeBinary(t *testing.T) string {
	t.Helper()
	name := managedOpenCodeBinaryName(runtime.GOOS)
	path := filepath.Join(t.TempDir(), name)
	script := "#!/bin/sh\necho 'It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing \"opencode-darwin-arm64\" package' >&2\nexit 1\n"
	require.NoError(t, os.WriteFile(path, []byte(script), 0o755))
	return path
}

func setManagedOpenCodeVersion(t *testing.T, version string) {
	t.Helper()
	oldVersion := ManagedOpenCodeVersion
	t.Cleanup(func() { ManagedOpenCodeVersion = oldVersion })
	ManagedOpenCodeVersion = version
}

func setManagedOpenCodeExecutable(t *testing.T, executable string) {
	t.Helper()
	oldExecutable := managedOpenCodeExecutable
	t.Cleanup(func() { managedOpenCodeExecutable = oldExecutable })
	managedOpenCodeExecutable = func() (string, error) { return executable, nil }
}
