package git

import (
	"context"
	"errors"
	"os"
	"os/exec"
	"strings"
	"testing"
	"time"

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

// envValue 从 "K=V" 形式的 env 列表中取出 key 对应的 value。
func envValue(env []string, key string) string {
	prefix := key + "="
	for _, e := range env {
		if strings.HasPrefix(e, prefix) {
			return strings.TrimPrefix(e, prefix)
		}
	}
	return ""
}

func TestDefaultRunner_exec_BinaryNotFound(t *testing.T) {
	t.Parallel()
	r := NewDefaultRunner(Config{Binary: "this-is-not-a-real-binary-xyz"})
	_, _, err := r.exec(context.Background(), "", nil, "--version")
	require.Error(t, err)
	var ge *GitError
	require.True(t, errors.As(err, &ge))
}

func TestDefaultRunner_exec_NonZeroExit(t *testing.T) {
	t.Parallel()
	gitAvailable(t)
	r := newTestRunner(t)
	_, _, err := r.exec(context.Background(), t.TempDir(), nil, "rev-parse", "HEAD")
	require.Error(t, err)
	var ge *GitError
	require.True(t, errors.As(err, &ge))
	require.NotZero(t, ge.ExitCode)
}

func TestDefaultRunner_exec_Timeout(t *testing.T) {
	t.Parallel()
	gitAvailable(t)
	r := newTestRunner(t)
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
	defer cancel()
	// 任意命令都会因 ctx 已过期而立刻被 kill
	_, _, err := r.exec(ctx, "", nil, "version")
	require.Error(t, err)
}

func TestGitError_Error_EmptyStderrFallsBackToCause(t *testing.T) {
	// 进程没跑起来时（chdir 失败 / 二进制缺失）stderr 为空，Error() 必须带上
	// Cause，否则落库的 last_sync_error 只剩 "exit -1: " 无法定位。
	t.Parallel()
	ge := &GitError{Args: []string{"fetch"}, ExitCode: -1,
		Cause: errors.New("chdir /data/x: no such file or directory")}
	require.Contains(t, ge.Error(), "no such file or directory")
	// stderr 非空时仍以 stderr 为准，不重复拼 Cause。
	ge2 := &GitError{Args: []string{"fetch"}, ExitCode: 128,
		Stderr: "fatal: boom", Cause: errors.New("exit status 128")}
	require.Contains(t, ge2.Error(), "fatal: boom")
	require.NotContains(t, ge2.Error(), "exit status 128")
}

func TestIsAuthError(t *testing.T) {
	t.Parallel()
	cases := []struct {
		stderr string
		want   bool
	}{
		{"Authentication failed for https://...", true},
		{"fatal: could not read Username for 'https://...'", true},
		{"Permission denied (publickey).", true},
		{"random other failure", false},
	}
	for _, c := range cases {
		got := IsAuthError(&GitError{Stderr: c.stderr})
		require.Equal(t, c.want, got, "stderr=%q", c.stderr)
	}
}

func TestCredentialEnv_None(t *testing.T) {
	t.Parallel()
	env, cleanup, err := credentialEnv(t.TempDir(), nil)
	require.NoError(t, err)
	require.Nil(t, env)
	cleanup() // 不应 panic
}

func TestCredentialEnv_PAT(t *testing.T) {
	t.Parallel()
	tmp := t.TempDir()
	env, cleanup, err := credentialEnv(tmp, &Credential{Kind: CredentialPAT, Username: "alice", Secret: []byte("tok")})
	require.NoError(t, err)
	defer cleanup()
	joined := strings.Join(env, "\n")
	require.Contains(t, joined, "GIT_ASKPASS=")
	require.Contains(t, joined, "GIT_TERMINAL_PROMPT=0")
	// 凭据通过 0600 临时文件下发（避免出现在进程 env 中），env 仅暴露文件路径，
	// 文件内容须等于用户名/令牌原文。
	require.Contains(t, joined, "GIT_PAT_USERNAME_FILE=")
	require.Contains(t, joined, "GIT_PAT_TOKEN_FILE=")
	userFile := envValue(env, "GIT_PAT_USERNAME_FILE")
	tokenFile := envValue(env, "GIT_PAT_TOKEN_FILE")
	require.NotEmpty(t, userFile)
	require.NotEmpty(t, tokenFile)
	userData, err := os.ReadFile(userFile)
	require.NoError(t, err)
	require.Equal(t, "alice", string(userData))
	tokenData, err := os.ReadFile(tokenFile)
	require.NoError(t, err)
	require.Equal(t, "tok", string(tokenData))
}

func TestCredentialEnv_SSHKey(t *testing.T) {
	t.Parallel()
	if _, err := exec.LookPath("ssh"); err != nil {
		t.Skip("ssh not available")
	}
	tmp := t.TempDir()
	env, cleanup, err := credentialEnv(tmp, &Credential{
		Kind:   CredentialSSHKey,
		Secret: []byte("-----BEGIN OPENSSH PRIVATE KEY-----\nstub\n-----END OPENSSH PRIVATE KEY-----\n"),
	})
	require.NoError(t, err)
	defer cleanup()
	require.Len(t, env, 1)
	require.True(t, strings.HasPrefix(env[0], "GIT_SSH_COMMAND="))
	require.Contains(t, env[0], "IdentitiesOnly=yes")
	require.Contains(t, env[0], "StrictHostKeyChecking=accept-new")
}
