package installpath

import (
	"errors"
	"os"
	"os/user"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
)

func TestEffectiveUserHome_Normal(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("SUDO_USER", "")

	require.Equal(t, home, effectiveUserHome())
}

func TestEffectiveUserHome_SudoFallsBackToCurrent(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("SUDO_USER", "nonexistent_user_who_does_not_exist_xyz")

	require.Equal(t, home, effectiveUserHome())
}

func TestEffectiveUserHome_SudoResolvesRealUser(t *testing.T) {
	current, err := user.Current()
	require.NoError(t, err)

	t.Setenv("SUDO_USER", current.Username)

	home, err := user.Lookup(current.Username)
	require.NoError(t, err)
	require.Equal(t, home.HomeDir, effectiveUserHome())
}

func TestManagedDir_RespectsSudoUser(t *testing.T) {
	current, err := user.Current()
	require.NoError(t, err)
	realHome, err := user.Lookup(current.Username)
	require.NoError(t, err)

	t.Setenv(profile.ConfigDirEnvKey, "")
	t.Setenv("SUDO_USER", current.Username)
	t.Setenv("HOME", "/root")

	expected := filepath.Join(realHome.HomeDir, ".agentteam", "bin")
	require.Equal(t, expected, ManagedDir())
}

func TestManagedBinaryPath_RespectsSudoUser(t *testing.T) {
	current, err := user.Current()
	require.NoError(t, err)
	realHome, err := user.Lookup(current.Username)
	require.NoError(t, err)

	t.Setenv(profile.ConfigDirEnvKey, "")
	t.Setenv("SUDO_USER", current.Username)
	t.Setenv("HOME", "/root")

	expected := filepath.Join(realHome.HomeDir, ".agentteam", "bin", "agentteam")
	require.Equal(t, expected, ManagedBinaryPath())
}

func TestManagedBinaryPathUsesAgentTeamConfigDir(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	require.Equal(t, filepath.Join(configDir, "bin", "agentteam"), ManagedBinaryPath())
}

func TestBinaryVersionUsesLongVersionFlag(t *testing.T) {
	binaryPath := filepath.Join(t.TempDir(), "agentteam")
	content := `#!/usr/bin/env bash
set -euo pipefail
if [[ "${1:-}" != "--version" || "${2:-}" != "-f" || "${3:-}" != "json" ]]; then
  echo "unexpected args: $*" >&2
  exit 2
fi
printf '{"version":"v1.2.3-test"}\n'
`
	require.NoError(t, os.WriteFile(binaryPath, []byte(content), 0755))

	version, err := binaryVersion(binaryPath)
	require.NoError(t, err)
	require.Equal(t, "v1.2.3-test", version)
}

func TestEnsureManagedBinaryCopiesNewerSource(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	sourcePath := filepath.Join(t.TempDir(), "agentteam-source")
	targetPath := ManagedBinaryPath()

	require.NoError(t, writeFakeBinary(sourcePath, "v1.0.2-rc.8"))
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v1.0.2-rc.1"))

	resolvedPath, err := EnsureManagedBinary(sourcePath)
	require.NoError(t, err)
	require.Equal(t, targetPath, resolvedPath)

	version, err := binaryVersion(targetPath)
	require.NoError(t, err)
	require.Equal(t, "v1.0.2-rc.8", version)
}

func TestEnsureManagedBinaryKeepsNewerTarget(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	sourcePath := filepath.Join(t.TempDir(), "agentteam-source")
	targetPath := ManagedBinaryPath()

	require.NoError(t, writeFakeBinary(sourcePath, "v1.0.2-rc.1"))
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v1.0.2-rc.8"))

	_, err := EnsureManagedBinary(sourcePath)
	require.NoError(t, err)

	version, err := binaryVersion(targetPath)
	require.NoError(t, err)
	require.Equal(t, "v1.0.2-rc.8", version)
}

func TestEnsureManagedBinaryReplacesSameVersionWhenContentDiffers(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	sourcePath := filepath.Join(t.TempDir(), "agentteam-source")
	targetPath := ManagedBinaryPath()

	require.NoError(t, writeFakeBinary(sourcePath, "v1.0.2-rc.8", "source"))
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v1.0.2-rc.8", "target"))

	_, err := EnsureManagedBinary(sourcePath)
	require.NoError(t, err)

	data, err := os.ReadFile(targetPath)
	require.NoError(t, err)
	require.Contains(t, string(data), "source")
}

func TestEnsureManagedBinaryCopiesBundledRuntimesWithBinary(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	sourceDir := t.TempDir()
	sourcePath := filepath.Join(sourceDir, "agentteam-source")
	targetPath := ManagedBinaryPath()

	require.NoError(t, writeFakeBinary(sourcePath, "v1.0.2-rc.8", "source"))
	require.NoError(t, writeRuntimeBinary(sourceDir, "managed-opencode"))
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v1.0.2-rc.1", "target"))

	resolvedPath, err := EnsureManagedBinary(sourcePath)
	require.NoError(t, err)
	require.Equal(t, targetPath, resolvedPath)

	data, err := os.ReadFile(filepath.Join(filepath.Dir(targetPath), "runtimes", "opencode", "opencode"))
	require.NoError(t, err)
	require.Contains(t, string(data), "managed-opencode")
}

func TestBundledRuntimeDirForMacOSHelper(t *testing.T) {
	helperPath := filepath.Join("/Applications", "AgentTeam.app", "Contents", "Helpers", "agentteam")
	require.Equal(t,
		filepath.Join("/Applications", "AgentTeam.app", "Contents", "Resources", "runtimes"),
		bundledRuntimeDirForBinary(helperPath),
	)
}

func TestEnsureManagedBinaryReplacesSymlink(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	sourcePath := filepath.Join(t.TempDir(), "agentteam-source")
	require.NoError(t, writeFakeBinary(sourcePath, "v1.2.0", "source"))
	targetPath := ManagedBinaryPath()
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, os.Symlink(sourcePath, targetPath))

	_, err := EnsureManagedBinary(sourcePath)
	require.NoError(t, err)
	info, err := os.Lstat(targetPath)
	require.NoError(t, err)
	require.True(t, info.Mode().IsRegular())
}

func TestEnsureManagedBinaryCopiesMacAppResourceRuntimesWithBundleExecutable(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	appContentsDir := filepath.Join(t.TempDir(), "AgentTeam.app", "Contents")
	sourcePath := filepath.Join(appContentsDir, "MacOS", "AgentTeam")
	targetPath := ManagedBinaryPath()

	require.NoError(t, os.MkdirAll(filepath.Dir(sourcePath), 0755))
	require.NoError(t, writeFakeBinary(sourcePath, "v1.0.2-rc.8", "source"))
	require.NoError(t, writeRuntimeBinary(filepath.Join(appContentsDir, "Resources"), "resource-opencode"))
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v1.0.2-rc.1", "target"))

	resolvedPath, err := EnsureManagedBinary(sourcePath)
	require.NoError(t, err)
	require.Equal(t, targetPath, resolvedPath)

	data, err := os.ReadFile(filepath.Join(filepath.Dir(targetPath), "runtimes", "opencode", "opencode"))
	require.NoError(t, err)
	require.Contains(t, string(data), "resource-opencode")
}

func TestEnsureManagedBinarySyncsBundledRuntimesWhenBinaryAlreadyCurrent(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	sourceDir := t.TempDir()
	sourcePath := filepath.Join(sourceDir, "agentteam-source")
	targetPath := ManagedBinaryPath()

	require.NoError(t, writeFakeBinary(sourcePath, "v1.0.2-rc.8", "same-build"))
	require.NoError(t, writeRuntimeBinary(sourceDir, "managed-opencode"))
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v1.0.2-rc.8", "same-build"))

	resolvedPath, err := EnsureManagedBinary(sourcePath)
	require.NoError(t, err)
	require.Equal(t, targetPath, resolvedPath)

	data, err := os.ReadFile(filepath.Join(filepath.Dir(targetPath), "runtimes", "opencode", "opencode"))
	require.NoError(t, err)
	require.Contains(t, string(data), "managed-opencode")
}

func TestEnsureManagedBinaryRestoresPreviousBinaryWhenRuntimeSyncFails(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	sourceDir := t.TempDir()
	sourcePath := filepath.Join(sourceDir, "agentteam-source")
	targetPath := ManagedBinaryPath()

	require.NoError(t, writeFakeBinary(sourcePath, "v1.0.2-rc.8", "source"))
	require.NoError(t, writeRuntimeBinary(sourceDir, "managed-opencode"))
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v1.0.2-rc.1", "target"))

	originalRename := runtimeTreeRename
	runtimeTreeRename = func(oldPath, newPath string) error {
		if newPath == filepath.Join(filepath.Dir(targetPath), "runtimes") {
			return errors.New("runtime rename failed")
		}
		return originalRename(oldPath, newPath)
	}
	t.Cleanup(func() { runtimeTreeRename = originalRename })

	_, err := EnsureManagedBinary(sourcePath)
	require.ErrorContains(t, err, "runtime rename failed")

	data, err := os.ReadFile(targetPath)
	require.NoError(t, err)
	require.Contains(t, string(data), "target")
	_, err = os.Stat(filepath.Join(filepath.Dir(targetPath), "runtimes"))
	require.True(t, os.IsNotExist(err), "runtime dir should not remain after failed sync: %v", err)
}

func TestEnsureShellPathWritesPreferredShellProfile(t *testing.T) {
	homeDir := t.TempDir()
	configDir := filepath.Join(homeDir, ".agentteam")
	t.Setenv("HOME", homeDir)
	t.Setenv("SHELL", "/bin/zsh")
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	require.NoError(t, EnsureShellPath())

	data, err := os.ReadFile(filepath.Join(homeDir, ".zshrc"))
	require.NoError(t, err)
	require.Contains(t, string(data), shellPathSnippetHeader)
	require.Contains(t, string(data), ManagedDir())
}

func TestForceEnsureManagedBinaryReplacesEvenWhenSourceIsOlder(t *testing.T) {
	// ForceEnsureManagedBinary must always replace the managed binary with the
	// source binary, even when the source has a lower version than the target.
	// This is the key behavioral difference from EnsureManagedBinary and the
	// fix for the bug where `agentteam service install` would silently keep the old
	// binary when the user ran a dev-build with a lower version string.
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	sourcePath := filepath.Join(t.TempDir(), "agentteam-source")
	targetPath := ManagedBinaryPath()

	// Source has LOWER version (v1.0.0) than target (v2.0.0).
	require.NoError(t, writeFakeBinary(sourcePath, "v1.0.0", "older-source"))
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v2.0.0", "newer-target"))

	resolvedPath, err := ForceEnsureManagedBinary(sourcePath)
	require.NoError(t, err)
	require.Equal(t, targetPath, resolvedPath)

	// The target must now contain the source content despite version downgrade.
	data, err := os.ReadFile(targetPath)
	require.NoError(t, err)
	require.Contains(t, string(data), "older-source")
}

func TestForceEnsureManagedBinaryReplacesSameVersion(t *testing.T) {
	// When source and target have the same version, ForceEnsureManagedBinary
	// should still replace (unlike EnsureManagedBinary which checks content).
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	sourcePath := filepath.Join(t.TempDir(), "agentteam-source")
	targetPath := ManagedBinaryPath()

	require.NoError(t, writeFakeBinary(sourcePath, "v1.0.0", "source-build"))
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v1.0.0", "existing-target"))

	resolvedPath, err := ForceEnsureManagedBinary(sourcePath)
	require.NoError(t, err)
	require.Equal(t, targetPath, resolvedPath)

	data, err := os.ReadFile(targetPath)
	require.NoError(t, err)
	require.Contains(t, string(data), "source-build")
}

func TestForceEnsureManagedBinaryShortCircuitsWhenAlreadyManaged(t *testing.T) {
	// When the source is already the managed binary, ForceEnsureManagedBinary
	// should return immediately without any copy operation.
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	targetPath := ManagedBinaryPath()
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v1.0.0", "canonical"))

	resolvedPath, err := ForceEnsureManagedBinary(targetPath)
	require.NoError(t, err)
	require.Equal(t, targetPath, resolvedPath)

	data, err := os.ReadFile(targetPath)
	require.NoError(t, err)
	require.Contains(t, string(data), "canonical")
}

func TestForceEnsureManagedBinaryCreatesTargetDir(t *testing.T) {
	// When the target directory doesn't exist yet, ForceEnsureManagedBinary
	// should create it and copy the binary.
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	sourcePath := filepath.Join(t.TempDir(), "agentteam-source")
	targetPath := ManagedBinaryPath()

	require.NoError(t, writeFakeBinary(sourcePath, "v0.1.0-dev", "fresh-install"))

	resolvedPath, err := ForceEnsureManagedBinary(sourcePath)
	require.NoError(t, err)
	require.Equal(t, targetPath, resolvedPath)

	data, err := os.ReadFile(targetPath)
	require.NoError(t, err)
	require.Contains(t, string(data), "fresh-install")
}

func TestEnsureManagedBinaryDoesNotReplaceOlderSource(t *testing.T) {
	// This test confirms the behavior that ForceEnsureManagedBinary fixes:
	// EnsureManagedBinary (non-force) keeps the newer target when source
	// has a lower version, which is correct for auto-upgrades but wrong for
	// explicit service install commands.
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	sourcePath := filepath.Join(t.TempDir(), "agentteam-source")
	targetPath := ManagedBinaryPath()

	require.NoError(t, writeFakeBinary(sourcePath, "v1.0.0", "older-source"))
	require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), 0755))
	require.NoError(t, writeFakeBinary(targetPath, "v2.0.0", "newer-target"))

	_, err := EnsureManagedBinary(sourcePath)
	require.NoError(t, err)

	// Non-force version keeps the newer target.
	data, err := os.ReadFile(targetPath)
	require.NoError(t, err)
	require.Contains(t, string(data), "newer-target")
}

func writeFakeBinary(path string, version string, marker ...string) error {
	body := "ok"
	if len(marker) > 0 && marker[0] != "" {
		body = marker[0]
	}
	content := "#!/usr/bin/env bash\nset -euo pipefail\nif [[ \"${1:-}\" == \"version\" || \"${1:-}\" == \"--version\" ]]; then\n  printf '{\"version\":\"" + version + "\"}\\n'\n  exit 0\nfi\nprintf '" + body + "\\n'\n"
	return os.WriteFile(path, []byte(content), 0755)
}

func writeRuntimeBinary(rootDir, marker string) error {
	runtimePath := filepath.Join(rootDir, "runtimes", "opencode", "opencode")
	if err := os.MkdirAll(filepath.Dir(runtimePath), 0755); err != nil {
		return err
	}
	content := "#!/usr/bin/env bash\nprintf '" + marker + "\\n'\n"
	return os.WriteFile(runtimePath, []byte(content), 0755)
}
