//go:build integration

package cmd

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

func TestIntegration_SetupChecksOutLatestReleaseAndContinuesWithNewBinary(t *testing.T) {
	repo := newSetupReleaseTestRepo(t, "cp release-cp9.sh bin/cp9\n\tchmod +x bin/cp9\n\ttouch build.marker")
	buildSetupTestBinary(t, repo)

	configPath := filepath.Join(repo, "missing.yaml")
	var stdout bytes.Buffer
	var stderr bytes.Buffer
	command := exec.Command(filepath.Join(repo, "bin", "cp9"), "setup", "config="+configPath)
	command.Dir = repo
	command.Env = append(os.Environ(), "HOME="+t.TempDir())
	command.Stdout = &stdout
	command.Stderr = &stderr
	if err := command.Run(); err != nil {
		t.Fatalf("setup: %v\nstdout:\n%s\nstderr:\n%s", err, stdout.String(), stderr.String())
	}

	if _, err := os.Stat(filepath.Join(repo, "build.marker")); err != nil {
		t.Fatalf("latest release was not built: %v", err)
	}
	args, err := os.ReadFile(filepath.Join(repo, "reexec.args"))
	if err != nil {
		t.Fatal(err)
	}
	if got, want := string(args), "setup\nconfig="+configPath+"\n"; got != want {
		t.Fatalf("new binary args = %q, want %q", got, want)
	}
	if got := strings.TrimSpace(gitOutput(t, repo, "describe", "--tags", "--exact-match")); got != "v1.0.0-beta.20260729" {
		t.Fatalf("checked out tag = %q, want latest release", got)
	}
}

func newSetupReleaseTestRepo(t *testing.T, buildRecipe string) string {
	t.Helper()

	repo := t.TempDir()
	git(t, repo, "init")
	git(t, repo, "config", "user.name", "CP9 Test")
	git(t, repo, "config", "user.email", "cp9@example.com")
	writeSetupReleaseTestFile(t, repo, ".gitignore", "/bin/\n/build.marker\n/reexec.args\n")
	writeSetupReleaseTestFile(t, repo, "Makefile", "build:\n\tfalse\n")
	git(t, repo, "add", ".")
	git(t, repo, "commit", "-m", "old")
	git(t, repo, "tag", "v1.0.0-beta.20260728")
	oldHead := strings.TrimSpace(gitOutput(t, repo, "rev-parse", "HEAD"))

	writeSetupReleaseTestFile(t, repo, "Makefile", "build:\n\t"+buildRecipe+"\n")
	writeSetupReleaseTestFile(t, repo, "release-cp9.sh", `#!/bin/sh
printf '%s\n' "$@" > reexec.args
`)
	git(t, repo, "add", ".")
	git(t, repo, "commit", "-m", "release")
	git(t, repo, "tag", "v1.0.0-beta.20260729")

	remote := filepath.Join(t.TempDir(), "origin.git")
	git(t, filepath.Dir(remote), "init", "--bare", remote)
	git(t, repo, "remote", "add", "origin", remote)
	git(t, repo, "push", "origin", "HEAD", "--tags")
	git(t, repo, "checkout", "--detach", oldHead)
	return repo
}

func TestIntegration_SetupSkipsBuildAtLatestRelease(t *testing.T) {
	repo := newSetupReleaseTestRepo(t, "cp release-cp9.sh bin/cp9\n\tchmod +x bin/cp9\n\ttouch build.marker")
	git(t, repo, "checkout", "--detach", "v1.0.0-beta.20260729")
	buildSetupTestBinary(t, repo)

	stdout, stderr, err := runSetupTestCommand(t, repo, "setup", "config="+filepath.Join(repo, "missing.yaml"))
	if err == nil {
		t.Fatal("setup with missing config unexpectedly succeeded")
	}
	if !strings.Contains(stderr, "already at latest release v1.0.0-beta.20260729; skipping build") {
		t.Fatalf("stderr does not explain skipped build:\n%s", stderr)
	}
	if _, statErr := os.Stat(filepath.Join(repo, "build.marker")); !os.IsNotExist(statErr) {
		t.Fatalf("latest release unexpectedly ran make build: %v", statErr)
	}
	if !strings.Contains(stdout, "cannot read config file") {
		t.Fatalf("existing setup did not continue after skipping build:\n%s", stdout)
	}
}

func TestIntegration_SetupCheckoutFalseUsesCurrentVersion(t *testing.T) {
	repo := newSetupReleaseTestRepo(t, "cp release-cp9.sh bin/cp9\n\tchmod +x bin/cp9\n\ttouch build.marker")
	buildSetupTestBinary(t, repo)
	oldHead := strings.TrimSpace(gitOutput(t, repo, "rev-parse", "HEAD"))

	stdout, stderr, err := runSetupTestCommand(
		t, repo, "setup", "checkout=false", "config="+filepath.Join(repo, "missing.yaml"),
	)
	if err == nil {
		t.Fatal("setup with missing config unexpectedly succeeded")
	}
	if got := strings.TrimSpace(gitOutput(t, repo, "rev-parse", "HEAD")); got != oldHead {
		t.Fatalf("checkout=false changed HEAD from %s to %s", oldHead, got)
	}
	if _, statErr := os.Stat(filepath.Join(repo, "build.marker")); !os.IsNotExist(statErr) {
		t.Fatalf("checkout=false unexpectedly ran make build: %v", statErr)
	}
	if strings.Contains(stderr, "cp9 setup:") {
		t.Fatalf("checkout=false unexpectedly ran release bootstrap:\n%s", stderr)
	}
	if !strings.Contains(stdout, "cannot read config file") {
		t.Fatalf("current setup did not run:\n%s", stdout)
	}
}

func TestIntegration_SetupPreservesDirtyWorktree(t *testing.T) {
	repo := newSetupReleaseTestRepo(t, "cp release-cp9.sh bin/cp9\n\tchmod +x bin/cp9\n\ttouch build.marker")
	buildSetupTestBinary(t, repo)
	oldHead := strings.TrimSpace(gitOutput(t, repo, "rev-parse", "HEAD"))
	writeSetupReleaseTestFile(t, repo, "Makefile", "local work\n")

	stdout, stderr, err := runSetupTestCommand(t, repo, "setup")
	if err == nil {
		t.Fatal("setup with dirty worktree unexpectedly succeeded")
	}
	if got := strings.TrimSpace(gitOutput(t, repo, "rev-parse", "HEAD")); got != oldHead {
		t.Fatalf("dirty setup changed HEAD from %s to %s", oldHead, got)
	}
	combined := stdout + stderr
	for _, expected := range []string{"working tree is not clean", "M Makefile", "checkout=false"} {
		if !strings.Contains(combined, expected) {
			t.Fatalf("dirty-worktree error does not contain %q:\n%s", expected, combined)
		}
	}
}

func TestIntegration_SetupRestoresPreviousHeadWhenBuildFails(t *testing.T) {
	repo := newSetupReleaseTestRepo(t, "false")
	buildSetupTestBinary(t, repo)
	oldHead := strings.TrimSpace(gitOutput(t, repo, "rev-parse", "HEAD"))

	stdout, stderr, err := runSetupTestCommand(t, repo, "setup")
	if err == nil {
		t.Fatal("setup with failing build unexpectedly succeeded")
	}
	if got := strings.TrimSpace(gitOutput(t, repo, "rev-parse", "HEAD")); got != oldHead {
		t.Fatalf("failed build left HEAD at %s, want restored %s", got, oldHead)
	}
	combined := stdout + stderr
	for _, expected := range []string{"build v1.0.0-beta.20260729 failed", "restored " + oldHead} {
		if !strings.Contains(combined, expected) {
			t.Fatalf("build-failure error does not contain %q:\n%s", expected, combined)
		}
	}
}

func TestIntegration_SetupUsesLoggedInGHWhenGitRemoteAuthFails(t *testing.T) {
	realGit, err := exec.LookPath("git")
	if err != nil {
		t.Fatal(err)
	}
	for _, testCase := range []struct {
		name        string
		origin      string
		wantRewrite string
	}{
		{
			name:        "SSH origin",
			origin:      "git@github.com:tidbcloud/controlplane9.git",
			wantRewrite: "url.https://github.com/.insteadOf=git@github.com:",
		},
		{
			name:   "HTTPS origin",
			origin: "https://github.com/tidbcloud/controlplane9.git",
		},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			repo := newSetupReleaseTestRepo(t, "cp release-cp9.sh bin/cp9\n\tchmod +x bin/cp9\n\ttouch build.marker")
			buildSetupTestBinary(t, repo)
			localRemote := strings.TrimSpace(gitOutput(t, repo, "remote", "get-url", "origin"))
			git(t, repo, "remote", "set-url", "origin", testCase.origin)

			stubDir := t.TempDir()
			authLog := filepath.Join(stubDir, "auth.log")
			writeSetupReleaseTestFile(t, stubDir, "git", `#!/bin/sh
printf 'git %s\n' "$*" >> "$CP9_TEST_AUTH_LOG"
case "$*" in
  *ls-remote*)
    case "$*" in
      *"credential.helper=!gh auth git-credential"*)
        gh auth git-credential >/dev/null
        exec "$CP9_TEST_REAL_GIT" -C "$CP9_TEST_REPO" ls-remote --tags --refs --sort=-version:refname "$CP9_TEST_REMOTE" 'refs/tags/v*'
        ;;
      *)
        echo "Permission denied (publickey)." >&2
        exit 128
        ;;
    esac
    ;;
  *fetch*)
    case "$*" in
      *"credential.helper=!gh auth git-credential"*)
        gh auth git-credential >/dev/null
        for argument in "$@"; do refspec="$argument"; done
        exec "$CP9_TEST_REAL_GIT" -C "$CP9_TEST_REPO" fetch --force "$CP9_TEST_REMOTE" "$refspec"
        ;;
      *)
        echo "Permission denied (publickey)." >&2
        exit 128
        ;;
    esac
    ;;
esac
exec "$CP9_TEST_REAL_GIT" "$@"
`)
			writeSetupReleaseTestFile(t, stubDir, "gh", `#!/bin/sh
printf 'gh %s\n' "$*" >> "$CP9_TEST_AUTH_LOG"
case "$1 $2" in
  "auth status"|"auth git-credential")
    exit 0
    ;;
esac
exit 1
`)

			home := t.TempDir()
			var stdout bytes.Buffer
			var stderr bytes.Buffer
			command := exec.Command(filepath.Join(repo, "bin", "cp9"), "setup")
			command.Dir = repo
			command.Env = append(os.Environ(),
				"PATH="+stubDir+string(os.PathListSeparator)+os.Getenv("PATH"),
				"HOME="+home,
				"CP9_TEST_AUTH_LOG="+authLog,
				"CP9_TEST_REAL_GIT="+realGit,
				"CP9_TEST_REPO="+repo,
				"CP9_TEST_REMOTE="+localRemote,
			)
			command.Stdout = &stdout
			command.Stderr = &stderr
			if err := command.Run(); err != nil {
				t.Fatalf("setup with gh fallback: %v\nstdout:\n%s\nstderr:\n%s",
					err, stdout.String(), stderr.String())
			}

			logData, err := os.ReadFile(authLog)
			if err != nil {
				t.Fatal(err)
			}
			logText := string(logData)
			for _, expected := range []string{
				"Permission denied",
				"credential.helper=!gh auth git-credential",
				"gh auth status",
				"gh auth git-credential",
			} {
				if !strings.Contains(stdout.String()+stderr.String()+logText, expected) {
					t.Fatalf("gh fallback evidence does not contain %q:\n%s\n%s\n%s",
						expected, stdout.String(), stderr.String(), logText)
				}
			}
			if testCase.wantRewrite == "" {
				if strings.Contains(logText, "insteadOf=") {
					t.Fatalf("HTTPS origin was unexpectedly rewritten:\n%s", logText)
				}
			} else if !strings.Contains(logText, testCase.wantRewrite) {
				t.Fatalf("SSH fallback does not contain %q:\n%s", testCase.wantRewrite, logText)
			}
			if _, err := os.Stat(filepath.Join(home, ".gitconfig")); !os.IsNotExist(err) {
				t.Fatalf("setup mutated global git config: %v", err)
			}
		})
	}
}

func buildSetupTestBinary(t *testing.T, repo string) {
	t.Helper()

	repoRoot, err := filepath.Abs(filepath.Join("..", ".."))
	if err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(repo, "bin"), 0o755); err != nil {
		t.Fatal(err)
	}
	command := exec.Command("go", "build", "-o", filepath.Join(repo, "bin", "cp9"), "./cmd/cp9")
	command.Dir = repoRoot
	if output, err := command.CombinedOutput(); err != nil {
		t.Fatalf("build cp9: %v\n%s", err, output)
	}
}

func runSetupTestCommand(t *testing.T, repo string, args ...string) (string, string, error) {
	t.Helper()
	var stdout bytes.Buffer
	var stderr bytes.Buffer
	command := exec.Command(filepath.Join(repo, "bin", "cp9"), args...)
	command.Dir = repo
	command.Env = append(os.Environ(), "HOME="+t.TempDir())
	command.Stdout = &stdout
	command.Stderr = &stderr
	err := command.Run()
	return stdout.String(), stderr.String(), err
}

func writeSetupReleaseTestFile(t *testing.T, repo, name, content string) {
	t.Helper()
	if err := os.WriteFile(filepath.Join(repo, name), []byte(content), 0o755); err != nil {
		t.Fatal(err)
	}
}

func git(t *testing.T, dir string, args ...string) {
	t.Helper()
	if output, err := exec.Command("git", append([]string{"-C", dir}, args...)...).CombinedOutput(); err != nil {
		t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, output)
	}
}

func gitOutput(t *testing.T, dir string, args ...string) string {
	t.Helper()
	output, err := exec.Command("git", append([]string{"-C", dir}, args...)...).CombinedOutput()
	if err != nil {
		t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, output)
	}
	return string(output)
}
