package scripts

import (
	"bytes"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"testing"
)

func requireMake(t *testing.T) {
	t.Helper()

	if _, err := exec.LookPath("make"); err != nil {
		t.Skip("make executable is required")
	}
}

func TestMakeHelpListsInstallTarget(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	cmd := exec.Command("make", "-C", desktopDir, "help")

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make help failed: %v, output=%s", err, string(output))
	}
	if !bytes.Contains(output, []byte("install")) {
		t.Fatalf("expected make help to list install target, got=%s", string(output))
	}
}

func TestMakeBuildDefaultsToArm64OnAppleSilicon(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	cmd := exec.Command(
		"make",
		"-n",
		"-C", desktopDir,
		"build",
		"HOST_OS=Darwin",
		"HOST_ARCH=arm64",
	)
	cmd.Env = withoutEnv(os.Environ(), "GOOS", "GOARCH", "CGO_ENABLED")

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make build dry-run failed: %v, output=%s", err, string(output))
	}
	if !bytes.Contains(output, []byte("GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build")) {
		t.Fatalf("expected Apple Silicon build env in dry-run, got=%s", string(output))
	}
}

func TestMakeBuildHonorsExplicitGoArchOnAppleSilicon(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	cmd := exec.Command(
		"make",
		"-n",
		"-C", desktopDir,
		"build",
		"HOST_OS=Darwin",
		"HOST_ARCH=arm64",
		"GOARCH=amd64",
	)
	cmd.Env = withoutEnv(os.Environ(), "GOOS", "GOARCH", "CGO_ENABLED")

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make build dry-run failed: %v, output=%s", err, string(output))
	}
	if bytes.Contains(output, []byte("GOARCH=arm64")) {
		t.Fatalf("expected explicit GOARCH to be honored, got=%s", string(output))
	}
}

func TestMakeBuildUsesCommittedIconAssetsWithoutRegenerating(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	cmd := exec.Command(
		"make",
		"-n",
		"-C", desktopDir,
		"build",
		"HOST_OS=Linux",
		"HOST_ARCH=x86_64",
	)

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make build dry-run failed: %v, output=%s", err, string(output))
	}
	if bytes.Contains(output, []byte("cmd/genicon")) {
		t.Fatalf("make build should not regenerate icon assets, got=%s", string(output))
	}
	if !bytes.Contains(output, []byte("go build")) {
		t.Fatalf("expected build dry-run to compile the desktop binary, got=%s", string(output))
	}
}

func TestMakeBuildDisablesGoVCSStamping(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	cmd := exec.Command(
		"make",
		"-n",
		"-C", desktopDir,
		"build",
		"HOST_OS=Linux",
		"HOST_ARCH=x86_64",
	)

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make build dry-run failed: %v, output=%s", err, string(output))
	}
	if !bytes.Contains(output, []byte("go build -buildvcs=false")) {
		t.Fatalf("expected desktop binary build to disable Go VCS stamping, got=%s", string(output))
	}
}

func TestMakeEnsureManagedOpenCodeInstallsSourceRuntime(t *testing.T) {
	t.Parallel()
	if runtime.GOOS == "windows" {
		t.Skip("Makefile runtime bundling target uses POSIX shell commands")
	}
	requireMake(t)

	desktopDir := desktopRootDir(t)
	sourceBinary := filepath.Join(t.TempDir(), "opencode")
	sourceContent := []byte{0x7f, 'E', 'L', 'F', '\n', 'f', 'a', 'k', 'e', '\n'}
	if err := os.WriteFile(sourceBinary, sourceContent, 0o755); err != nil {
		t.Fatalf("write managed OpenCode source: %v", err)
	}
	sourceModels := filepath.Join(t.TempDir(), "models.json")
	modelsContent := []byte(`{"local":{"id":"local","name":"Local","env":[],"models":{}}}` + "\n")
	if err := os.WriteFile(sourceModels, modelsContent, 0o644); err != nil {
		t.Fatalf("write managed OpenCode models source: %v", err)
	}
	binDir := filepath.Join(t.TempDir(), "bin")

	cmd := exec.Command(
		"make",
		"-C", desktopDir,
		"ensure-managed-opencode",
		"BINDIR="+binDir,
		"AGENTTEAM_BUILD_GOOS=linux",
		"AGENTTEAM_BUILD_GOARCH=amd64",
		"MANAGED_OPENCODE_SOURCE="+sourceBinary,
		"MANAGED_OPENCODE_VERSION=9.9.9",
		"MANAGED_OPENCODE_NPM_REGISTRY=https://example.invalid/npm",
		"MANAGED_OPENCODE_MODELS_SOURCE="+sourceModels,
	)
	cmd.Env = withoutEnv(os.Environ(),
		"GOOS",
		"GOARCH",
		"CGO_ENABLED",
		"AGENTTEAM_MANAGED_OPENCODE_SOURCE",
		"AGENTTEAM_MANAGED_OPENCODE_VERSION",
		"AGENTTEAM_MANAGED_OPENCODE_NPM_REGISTRY",
		"AGENTTEAM_MANAGED_OPENCODE_MODELS_SOURCE",
	)

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make ensure-managed-opencode failed: %v, output=%s", err, string(output))
	}

	runtimePath := filepath.Join(binDir, "runtimes", "opencode", "opencode")
	data, err := os.ReadFile(runtimePath)
	if err != nil {
		t.Fatalf("read bundled runtime: %v", err)
	}
	if !bytes.Equal(data, sourceContent) {
		t.Fatalf("bundled runtime content mismatch: got=%q want=%q", data, sourceContent)
	}
	stat, err := os.Stat(runtimePath)
	if err != nil {
		t.Fatalf("stat bundled runtime: %v", err)
	}
	if stat.Mode()&0111 == 0 {
		t.Fatalf("bundled runtime is not executable: mode=%v", stat.Mode())
	}
	if gotMode := stat.Mode().Perm(); gotMode != 0o755 {
		t.Fatalf("bundled runtime mode = %v, want 0755", gotMode)
	}
	modelsPath := filepath.Join(binDir, "runtimes", "opencode", "models.json")
	modelsData, err := os.ReadFile(modelsPath)
	if err != nil {
		t.Fatalf("read bundled models.json: %v", err)
	}
	if !bytes.Equal(modelsData, modelsContent) {
		t.Fatalf("bundled models.json content mismatch: got=%q want=%q", modelsData, modelsContent)
	}
	modelsStat, err := os.Stat(modelsPath)
	if err != nil {
		t.Fatalf("stat bundled models.json: %v", err)
	}
	if gotMode := modelsStat.Mode().Perm(); gotMode != 0o644 {
		t.Fatalf("bundled models.json mode = %v, want 0644", gotMode)
	}
}

func TestMakeEnsureManagedOpenCodeDownloadsDefaultModelsCatalog(t *testing.T) {
	t.Parallel()
	if runtime.GOOS == "windows" {
		t.Skip("Makefile runtime bundling target uses POSIX shell commands")
	}
	requireMake(t)

	desktopDir := desktopRootDir(t)
	sourceBinary := filepath.Join(t.TempDir(), "opencode")
	sourceContent := []byte{0x7f, 'E', 'L', 'F', '\n', 'f', 'a', 'k', 'e', '\n'}
	if err := os.WriteFile(sourceBinary, sourceContent, 0o755); err != nil {
		t.Fatalf("write managed OpenCode source: %v", err)
	}

	fakeBin := t.TempDir()
	modelsContent := []byte(`{"downloaded":{"id":"downloaded","name":"Downloaded","env":[],"models":{}}}` + "\n")
	curlPath := filepath.Join(fakeBin, "curl")
	curlScript := "#!/usr/bin/env bash\nset -euo pipefail\nout=\"\"\nwhile [ $# -gt 0 ]; do\n  case \"$1\" in\n    -o) out=\"$2\"; shift 2 ;;\n    *) shift ;;\n  esac\ndone\n[ -n \"$out\" ] || { echo 'missing curl output' >&2; exit 1; }\nprintf '%s' '" + string(modelsContent) + "' > \"$out\"\n"
	if err := os.WriteFile(curlPath, []byte(curlScript), 0o755); err != nil {
		t.Fatalf("write fake curl: %v", err)
	}

	binDir := filepath.Join(t.TempDir(), "bin")
	cacheDir := filepath.Join(t.TempDir(), "cache")

	cmd := exec.Command(
		"make",
		"-C", desktopDir,
		"ensure-managed-opencode",
		"BINDIR="+binDir,
		"AGENTTEAM_BUILD_GOOS=linux",
		"AGENTTEAM_BUILD_GOARCH=amd64",
		"MANAGED_OPENCODE_SOURCE="+sourceBinary,
		"MANAGED_OPENCODE_VERSION=9.9.9",
		"MANAGED_OPENCODE_NPM_REGISTRY=https://example.invalid/npm",
		"MANAGED_OPENCODE_MODELS_URL=https://models.invalid/api.json",
		"MANAGED_OPENCODE_CACHE_DIR="+cacheDir,
	)
	cmd.Env = withoutEnv(os.Environ(),
		"GOOS",
		"GOARCH",
		"CGO_ENABLED",
		"AGENTTEAM_MANAGED_OPENCODE_SOURCE",
		"AGENTTEAM_MANAGED_OPENCODE_VERSION",
		"AGENTTEAM_MANAGED_OPENCODE_NPM_REGISTRY",
		"AGENTTEAM_MANAGED_OPENCODE_MODELS_SOURCE",
		"AGENTTEAM_MANAGED_OPENCODE_MODELS_URL",
		"AGENTTEAM_MANAGED_OPENCODE_CACHE_DIR",
	)
	cmd.Env = append(cmd.Env, "PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"))

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make ensure-managed-opencode failed: %v, output=%s", err, string(output))
	}

	modelsPath := filepath.Join(binDir, "runtimes", "opencode", "models.json")
	modelsData, err := os.ReadFile(modelsPath)
	if err != nil {
		t.Fatalf("read bundled models.json: %v", err)
	}
	if !bytes.Equal(modelsData, modelsContent) {
		t.Fatalf("bundled models.json content mismatch: got=%q want=%q", modelsData, modelsContent)
	}

	cachedPath := filepath.Join(cacheDir, "9.9.9", "models.json")
	cachedData, err := os.ReadFile(cachedPath)
	if err != nil {
		t.Fatalf("read cached models.json: %v", err)
	}
	if !bytes.Equal(cachedData, modelsContent) {
		t.Fatalf("cached models.json content mismatch: got=%q want=%q", cachedData, modelsContent)
	}
}

func TestMakeManagedOpenCodePackageFollowsBuildTarget(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	cmd := exec.Command(
		"make",
		"-n",
		"-C", desktopDir,
		"ensure-managed-opencode",
		"HOST_OS=Darwin",
		"HOST_ARCH=arm64",
		"GOARCH=amd64",
		"MANAGED_OPENCODE_VERSION=9.9.9",
		"MANAGED_OPENCODE_NPM_REGISTRY=https://example.invalid/npm",
	)
	cmd.Env = withoutEnv(os.Environ(), "GOOS", "GOARCH", "CGO_ENABLED")

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make ensure-managed-opencode dry-run failed: %v, output=%s", err, string(output))
	}
	if !bytes.Contains(output, []byte("opencode-darwin-x64@9.9.9")) {
		t.Fatalf("expected darwin amd64 managed OpenCode package in dry-run, got=%s", string(output))
	}
	if bytes.Contains(output, []byte("opencode-darwin-arm64@9.9.9")) {
		t.Fatalf("managed OpenCode package should follow GOARCH override, got=%s", string(output))
	}
}

func TestMakeManagedOpenCodePackageSupportsWindowsArm64(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	cmd := exec.Command(
		"make",
		"-n",
		"-C", desktopDir,
		"ensure-managed-opencode",
		"AGENTTEAM_BUILD_GOOS=windows",
		"AGENTTEAM_BUILD_GOARCH=arm64",
		"MANAGED_OPENCODE_VERSION=9.9.9",
		"MANAGED_OPENCODE_NPM_REGISTRY=https://example.invalid/npm",
	)
	cmd.Env = withoutEnv(os.Environ(), "GOOS", "GOARCH", "CGO_ENABLED")

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make ensure-managed-opencode Windows arm64 dry-run failed: %v, output=%s", err, string(output))
	}
	if !bytes.Contains(output, []byte("opencode-windows-arm64@9.9.9")) {
		t.Fatalf("expected Windows arm64 managed OpenCode package in dry-run, got=%s", string(output))
	}
	if bytes.Contains(output, []byte("opencode-windows-x64@9.9.9")) {
		t.Fatalf("Windows arm64 build must not use x64 managed OpenCode, got=%s", string(output))
	}
}

func TestMakeGenerateIconsUsesElectronNativeAssets(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	cmd := exec.Command(
		"make",
		"-n",
		"-C", desktopDir,
		"generate-icons",
	)

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make generate-icons dry-run failed: %v, output=%s", err, string(output))
	}
	if !bytes.Contains(output, []byte("go run ./cmd/genicon -native")) {
		t.Fatalf("expected Electron native icon generation in dry-run, got=%s", string(output))
	}
	if bytes.Contains(output, []byte(".syso")) {
		t.Fatalf("Electron icon generation must not produce Go resource objects, got=%s", string(output))
	}
}

func TestMakeInstallInstallsProvidedBinary(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	installDir := t.TempDir()
	sourceBinary := filepath.Join(t.TempDir(), "agentteam")
	if err := os.WriteFile(sourceBinary, []byte("#!/usr/bin/env bash\necho agentteam\n"), 0755); err != nil {
		t.Fatalf("write source binary: %v", err)
	}

	cmd := exec.Command(
		"make",
		"-C", desktopDir,
		"install",
		"INSTALL_SOURCE="+sourceBinary,
		"INSTALL_DIR="+installDir,
	)

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make install failed: %v, output=%s", err, string(output))
	}

	installedBinary := filepath.Join(installDir, "agentteam")
	stat, err := os.Stat(installedBinary)
	if err != nil {
		t.Fatalf("stat installed binary: %v", err)
	}
	if stat.Mode()&0111 == 0 {
		t.Fatalf("installed binary is not executable: mode=%v", stat.Mode())
	}
}

func TestMakeInstallCopiesBundledRuntimes(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	installDir := t.TempDir()
	sourceDir := t.TempDir()
	sourceBinary := filepath.Join(sourceDir, "agentteam")
	if err := os.WriteFile(sourceBinary, []byte("#!/usr/bin/env bash\necho agentteam\n"), 0o755); err != nil {
		t.Fatalf("write source binary: %v", err)
	}
	sourceRuntime := filepath.Join(sourceDir, "runtimes", "opencode", "opencode")
	runtimeContent := []byte("#!/usr/bin/env bash\necho opencode\n")
	if err := os.MkdirAll(filepath.Dir(sourceRuntime), 0o755); err != nil {
		t.Fatalf("mkdir source runtime dir: %v", err)
	}
	if err := os.WriteFile(sourceRuntime, runtimeContent, 0o755); err != nil {
		t.Fatalf("write source runtime: %v", err)
	}
	sourceModels := filepath.Join(sourceDir, "runtimes", "opencode", "models.json")
	modelsContent := []byte(`{"local":{"id":"local","name":"Local","env":[],"models":{}}}` + "\n")
	if err := os.WriteFile(sourceModels, modelsContent, 0o644); err != nil {
		t.Fatalf("write source models: %v", err)
	}

	cmd := exec.Command(
		"make",
		"-C", desktopDir,
		"install",
		"INSTALL_SOURCE="+sourceBinary,
		"INSTALL_DIR="+installDir,
	)

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make install failed: %v, output=%s", err, string(output))
	}

	installedRuntime := filepath.Join(installDir, "runtimes", "opencode", "opencode")
	data, err := os.ReadFile(installedRuntime)
	if err != nil {
		t.Fatalf("read installed runtime: %v", err)
	}
	if !bytes.Equal(data, runtimeContent) {
		t.Fatalf("installed runtime content mismatch: got=%q want=%q", data, runtimeContent)
	}
	stat, err := os.Stat(installedRuntime)
	if err != nil {
		t.Fatalf("stat installed runtime: %v", err)
	}
	if stat.Mode()&0111 == 0 {
		t.Fatalf("installed runtime is not executable: mode=%v", stat.Mode())
	}
	if gotMode := stat.Mode().Perm(); gotMode != 0o755 {
		t.Fatalf("installed runtime mode = %v, want 0755", gotMode)
	}
	installedModels := filepath.Join(installDir, "runtimes", "opencode", "models.json")
	modelsData, err := os.ReadFile(installedModels)
	if err != nil {
		t.Fatalf("read installed models.json: %v", err)
	}
	if !bytes.Equal(modelsData, modelsContent) {
		t.Fatalf("installed models.json content mismatch: got=%q want=%q", modelsData, modelsContent)
	}
}

func TestMakeInstallDefaultsToAgentTeamBin(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	homeDir := t.TempDir()
	goPathDir := t.TempDir()
	sourceBinary := filepath.Join(t.TempDir(), "agentteam")
	if err := os.WriteFile(sourceBinary, []byte("#!/usr/bin/env bash\necho agentteam\n"), 0755); err != nil {
		t.Fatalf("write source binary: %v", err)
	}

	cmd := exec.Command(
		"make",
		"-C", desktopDir,
		"install",
		"INSTALL_SOURCE="+sourceBinary,
	)
	cmd.Env = append(os.Environ(),
		"HOME="+homeDir,
		"GOBIN=",
		"GOPATH="+goPathDir,
	)

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make install failed: %v, output=%s", err, string(output))
	}

	installedBinary := filepath.Join(homeDir, ".agentteam", "bin", "agentteam")
	if _, err := os.Stat(installedBinary); err != nil {
		t.Fatalf("expected binary in ~/.agentteam/bin, stat %s: %v", installedBinary, err)
	}
	goPathBinary := filepath.Join(goPathDir, "bin", "agentteam")
	if _, err := os.Stat(goPathBinary); !os.IsNotExist(err) {
		t.Fatalf("expected no binary in GOPATH/bin, stat error=%v", err)
	}
}

func TestMakeInstallIgnoresGoBinByDefault(t *testing.T) {
	t.Parallel()
	requireMake(t)

	desktopDir := desktopRootDir(t)
	homeDir := t.TempDir()
	goBinDir := t.TempDir()
	goPathDir := t.TempDir()
	sourceBinary := filepath.Join(t.TempDir(), "agentteam")
	if err := os.WriteFile(sourceBinary, []byte("#!/usr/bin/env bash\necho agentteam\n"), 0755); err != nil {
		t.Fatalf("write source binary: %v", err)
	}

	cmd := exec.Command(
		"make",
		"-C", desktopDir,
		"install",
		"INSTALL_SOURCE="+sourceBinary,
	)
	cmd.Env = append(os.Environ(),
		"HOME="+homeDir,
		"GOBIN="+goBinDir,
		"GOPATH="+goPathDir,
	)

	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("make install failed: %v, output=%s", err, string(output))
	}

	installedBinary := filepath.Join(homeDir, ".agentteam", "bin", "agentteam")
	if _, err := os.Stat(installedBinary); err != nil {
		t.Fatalf("expected binary in ~/.agentteam/bin, stat %s: %v", installedBinary, err)
	}
	goBinBinary := filepath.Join(goBinDir, "agentteam")
	if _, err := os.Stat(goBinBinary); !os.IsNotExist(err) {
		t.Fatalf("expected no binary in GOBIN, stat error=%v", err)
	}
}

func desktopRootDir(t *testing.T) string {
	t.Helper()

	dir, err := filepath.Abs("..")
	if err != nil {
		t.Fatalf("resolve desktop root: %v", err)
	}
	return dir
}

func withoutEnv(env []string, keys ...string) []string {
	blocked := make(map[string]struct{}, len(keys))
	for _, key := range keys {
		blocked[key] = struct{}{}
	}
	filtered := make([]string, 0, len(env))
	for _, item := range env {
		name := item
		if idx := bytes.IndexByte([]byte(item), '='); idx >= 0 {
			name = item[:idx]
		}
		if _, ok := blocked[name]; ok {
			continue
		}
		filtered = append(filtered, item)
	}
	return filtered
}
