package git

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"testing"

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

func TestClientCommand(t *testing.T) {
	tests := []struct {
		name     string
		repoDir  string
		gitPath  string
		wantExe  string
		wantArgs []string
	}{
		{
			name:     "creates command",
			gitPath:  "path/to/git",
			wantExe:  "path/to/git",
			wantArgs: []string{"path/to/git", "ref-log"},
		},
		{
			name:     "adds repo directory configuration",
			repoDir:  "path/to/repo",
			gitPath:  "path/to/git",
			wantExe:  "path/to/git",
			wantArgs: []string{"path/to/git", "-C", "path/to/repo", "ref-log"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			in, out, errOut := &bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{}
			client := Client{
				Stdin:   in,
				Stdout:  out,
				Stderr:  errOut,
				RepoDir: tt.repoDir,
				GitPath: tt.gitPath,
			}
			cmd, err := client.Command(context.Background(), "ref-log")
			assert.NoError(t, err)
			assert.Equal(t, tt.wantExe, cmd.Path)
			assert.Equal(t, tt.wantArgs, cmd.Args)
			assert.Equal(t, in, cmd.Stdin)
			assert.Equal(t, out, cmd.Stdout)
			assert.Equal(t, errOut, cmd.Stderr)
		})
	}
}

func TestClientAuthenticatedCommand(t *testing.T) {
	tests := []struct {
		name     string
		path     string
		wantArgs []string
	}{
		{
			name:     "adds credential helper config options",
			path:     "path/to/gh",
			wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", `credential.helper=!"path/to/gh" auth git-credential`, "fetch"},
		},
		{
			name:     "fallback when GhPath is not set",
			wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", `credential.helper=!"gh" auth git-credential`, "fetch"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			client := Client{
				GhPath:  tt.path,
				GitPath: "path/to/git",
			}
			cmd, err := client.AuthenticatedCommand(context.Background(), "fetch")
			assert.NoError(t, err)
			assert.Equal(t, tt.wantArgs, cmd.Args)
		})
	}
}

func TestClientRemotes(t *testing.T) {
	tempDir := t.TempDir()
	initRepo(t, tempDir)
	gitDir := filepath.Join(tempDir, ".git")
	remoteFile := filepath.Join(gitDir, "config")
	remotes := `
[remote "origin"]
	url = git@example.com:monalisa/origin.git
[remote "test"]
	url = git://github.com/hubot/test.git
	gh-resolved = other
[remote "upstream"]
	url = https://github.com/monalisa/upstream.git
	gh-resolved = base
[remote "github"]
	url = git@github.com:hubot/github.git
`
	f, err := os.OpenFile(remoteFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755)
	assert.NoError(t, err)
	_, err = f.Write([]byte(remotes))
	assert.NoError(t, err)
	err = f.Close()
	assert.NoError(t, err)
	client := Client{
		RepoDir: tempDir,
	}
	rs, err := client.Remotes(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, 4, len(rs))
	assert.Equal(t, "upstream", rs[0].Name)
	assert.Equal(t, "base", rs[0].Resolved)
	assert.Equal(t, "github", rs[1].Name)
	assert.Equal(t, "", rs[1].Resolved)
	assert.Equal(t, "origin", rs[2].Name)
	assert.Equal(t, "", rs[2].Resolved)
	assert.Equal(t, "test", rs[3].Name)
	assert.Equal(t, "other", rs[3].Resolved)
}

func TestClientRemotes_no_resolved_remote(t *testing.T) {
	tempDir := t.TempDir()
	initRepo(t, tempDir)
	gitDir := filepath.Join(tempDir, ".git")
	remoteFile := filepath.Join(gitDir, "config")
	remotes := `
[remote "origin"]
	url = git@example.com:monalisa/origin.git
[remote "test"]
	url = git://github.com/hubot/test.git
[remote "upstream"]
	url = https://github.com/monalisa/upstream.git
[remote "github"]
	url = git@github.com:hubot/github.git
`
	f, err := os.OpenFile(remoteFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755)
	assert.NoError(t, err)
	_, err = f.Write([]byte(remotes))
	assert.NoError(t, err)
	err = f.Close()
	assert.NoError(t, err)
	client := Client{
		RepoDir: tempDir,
	}
	rs, err := client.Remotes(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, 4, len(rs))
	assert.Equal(t, "upstream", rs[0].Name)
	assert.Equal(t, "github", rs[1].Name)
	assert.Equal(t, "origin", rs[2].Name)
	assert.Equal(t, "", rs[2].Resolved)
	assert.Equal(t, "test", rs[3].Name)
}

func TestParseRemotes(t *testing.T) {
	remoteList := []string{
		"mona\tgit@github.com:monalisa/myfork.git (fetch)",
		"origin\thttps://github.com/monalisa/octo-cat.git (fetch)",
		"origin\thttps://github.com/monalisa/octo-cat-push.git (push)",
		"upstream\thttps://example.com/nowhere.git (fetch)",
		"upstream\thttps://github.com/hubot/tools (push)",
		"zardoz\thttps://example.com/zed.git (push)",
		"koke\tgit://github.com/koke/grit.git (fetch)",
		"koke\tgit://github.com/koke/grit.git (push)",
	}

	r := parseRemotes(remoteList)
	assert.Equal(t, 5, len(r))

	assert.Equal(t, "mona", r[0].Name)
	assert.Equal(t, "ssh://git@github.com/monalisa/myfork.git", r[0].FetchURL.String())
	assert.Nil(t, r[0].PushURL)

	assert.Equal(t, "origin", r[1].Name)
	assert.Equal(t, "/monalisa/octo-cat.git", r[1].FetchURL.Path)
	assert.Equal(t, "/monalisa/octo-cat-push.git", r[1].PushURL.Path)

	assert.Equal(t, "upstream", r[2].Name)
	assert.Equal(t, "example.com", r[2].FetchURL.Host)
	assert.Equal(t, "github.com", r[2].PushURL.Host)

	assert.Equal(t, "zardoz", r[3].Name)
	assert.Nil(t, r[3].FetchURL)
	assert.Equal(t, "https://example.com/zed.git", r[3].PushURL.String())

	assert.Equal(t, "koke", r[4].Name)
	assert.Equal(t, "/koke/grit.git", r[4].FetchURL.Path)
	assert.Equal(t, "/koke/grit.git", r[4].PushURL.Path)
}

func TestClientUpdateRemoteURL(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
	}{
		{
			name:        "update remote url",
			wantCmdArgs: `path/to/git remote set-url test https://test.com`,
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git remote set-url test https://test.com`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			err := client.UpdateRemoteURL(context.Background(), "test", "https://test.com")
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
		})
	}
}

func TestClientSetRemoteResolution(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
	}{
		{
			name:        "set remote resolution",
			wantCmdArgs: `path/to/git config --add remote.origin.gh-resolved base`,
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git config --add remote.origin.gh-resolved base`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			err := client.SetRemoteResolution(context.Background(), "origin", "base")
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
		})
	}
}

func TestClientCurrentBranch(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
		wantBranch    string
	}{
		{
			name:        "branch name",
			cmdStdout:   "branch-name\n",
			wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`,
			wantBranch:  "branch-name",
		},
		{
			name:        "ref",
			cmdStdout:   "refs/heads/branch-name\n",
			wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`,
			wantBranch:  "branch-name",
		},
		{
			name:        "escaped ref",
			cmdStdout:   "refs/heads/branch\u00A0with\u00A0non\u00A0breaking\u00A0space\n",
			wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`,
			wantBranch:  "branch\u00A0with\u00A0non\u00A0breaking\u00A0space",
		},
		{
			name:          "detatched head",
			cmdExitStatus: 1,
			wantCmdArgs:   `path/to/git symbolic-ref --quiet HEAD`,
			wantErrorMsg:  "failed to run git: not on any branch",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			branch, err := client.CurrentBranch(context.Background())
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
			assert.Equal(t, tt.wantBranch, branch)
		})
	}
}

func TestClientShowRefs(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantRefs      []Ref
		wantErrorMsg  string
	}{
		{
			name:          "show refs with one vaid ref and one invalid ref",
			cmdExitStatus: 128,
			cmdStdout:     "9ea76237a557015e73446d33268569a114c0649c refs/heads/valid",
			cmdStderr:     "fatal: 'refs/heads/invalid' - not a valid ref",
			wantCmdArgs:   `path/to/git show-ref --verify -- refs/heads/valid refs/heads/invalid`,
			wantRefs: []Ref{{
				Hash: "9ea76237a557015e73446d33268569a114c0649c",
				Name: "refs/heads/valid",
			}},
			wantErrorMsg: "failed to run git: fatal: 'refs/heads/invalid' - not a valid ref",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			refs, err := client.ShowRefs(context.Background(), []string{"refs/heads/valid", "refs/heads/invalid"})
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			assert.EqualError(t, err, tt.wantErrorMsg)
			assert.Equal(t, tt.wantRefs, refs)
		})
	}
}

func TestClientConfig(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantOut       string
		wantErrorMsg  string
	}{
		{
			name:        "get config key",
			cmdStdout:   "test",
			wantCmdArgs: `path/to/git config credential.helper`,
			wantOut:     "test",
		},
		{
			name:          "get unknown config key",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git config credential.helper`,
			wantErrorMsg:  "failed to run git: unknown config key credential.helper",
		},
		{
			name:          "git error",
			cmdExitStatus: 2,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git config credential.helper`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			out, err := client.Config(context.Background(), "credential.helper")
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
			assert.Equal(t, tt.wantOut, out)
		})
	}
}

func TestClientUncommittedChangeCount(t *testing.T) {
	tests := []struct {
		name            string
		cmdExitStatus   int
		cmdStdout       string
		cmdStderr       string
		wantCmdArgs     string
		wantChangeCount int
	}{
		{
			name:            "no changes",
			wantCmdArgs:     `path/to/git status --porcelain`,
			wantChangeCount: 0,
		},
		{
			name:            "one change",
			cmdStdout:       " M poem.txt",
			wantCmdArgs:     `path/to/git status --porcelain`,
			wantChangeCount: 1,
		},
		{
			name:            "untracked file",
			cmdStdout:       " M poem.txt\n?? new.txt",
			wantCmdArgs:     `path/to/git status --porcelain`,
			wantChangeCount: 2,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			ucc, err := client.UncommittedChangeCount(context.Background())
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			assert.NoError(t, err)
			assert.Equal(t, tt.wantChangeCount, ucc)
		})
	}
}

func TestClientCommits(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantCommits   []*Commit
		wantErrorMsg  string
	}{
		{
			name:        "get commits",
			cmdStdout:   "6a6872b918c601a0e730710ad8473938a7516d30,testing testability test",
			wantCmdArgs: `path/to/git -c log.ShowSignature=false log --pretty=format:%H,%s --cherry SHA1...SHA2`,
			wantCommits: []*Commit{{
				Sha:   "6a6872b918c601a0e730710ad8473938a7516d30",
				Title: "testing testability test",
			}},
		},
		{
			name:         "no commits between SHAs",
			wantCmdArgs:  `path/to/git -c log.ShowSignature=false log --pretty=format:%H,%s --cherry SHA1...SHA2`,
			wantErrorMsg: "could not find any commits between SHA1 and SHA2",
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git -c log.ShowSignature=false log --pretty=format:%H,%s --cherry SHA1...SHA2`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			commits, err := client.Commits(context.Background(), "SHA1", "SHA2")
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg != "" {
				assert.EqualError(t, err, tt.wantErrorMsg)
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, tt.wantCommits, commits)
		})
	}
}

func TestClientLastCommit(t *testing.T) {
	client := Client{
		RepoDir: "./fixtures/simple.git",
	}
	c, err := client.LastCommit(context.Background())
	assert.NoError(t, err)
	assert.Equal(t, "6f1a2405cace1633d89a79c74c65f22fe78f9659", c.Sha)
	assert.Equal(t, "Second commit", c.Title)
}

func TestClientCommitBody(t *testing.T) {
	client := Client{
		RepoDir: "./fixtures/simple.git",
	}
	body, err := client.CommitBody(context.Background(), "6f1a2405cace1633d89a79c74c65f22fe78f9659")
	assert.NoError(t, err)
	assert.Equal(t, "I'm starting to get the hang of things\n", body)
}

func TestClientReadBranchConfig(t *testing.T) {
	tests := []struct {
		name             string
		cmdExitStatus    int
		cmdStdout        string
		cmdStderr        string
		wantCmdArgs      string
		wantBranchConfig BranchConfig
	}{
		{
			name:             "read branch config",
			cmdStdout:        "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk",
			wantCmdArgs:      `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge)$`,
			wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			branchConfig := client.ReadBranchConfig(context.Background(), "trunk")
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			assert.Equal(t, tt.wantBranchConfig, branchConfig)
		})
	}
}

func TestClientDeleteLocalBranch(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
	}{
		{
			name:        "delete local branch",
			wantCmdArgs: `path/to/git branch -D trunk`,
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git branch -D trunk`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			err := client.DeleteLocalBranch(context.Background(), "trunk")
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
		})
	}
}

func TestClientHasLocalBranch(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantOut       bool
	}{
		{
			name:        "has local branch",
			wantCmdArgs: `path/to/git rev-parse --verify refs/heads/trunk`,
			wantOut:     true,
		},
		{
			name:          "does not have local branch",
			cmdExitStatus: 1,
			wantCmdArgs:   `path/to/git rev-parse --verify refs/heads/trunk`,
			wantOut:       false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			out := client.HasLocalBranch(context.Background(), "trunk")
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			assert.Equal(t, out, tt.wantOut)
		})
	}
}

func TestClientCheckoutBranch(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
	}{
		{
			name:        "checkout branch",
			wantCmdArgs: `path/to/git checkout trunk`,
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git checkout trunk`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			err := client.CheckoutBranch(context.Background(), "trunk")
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
		})
	}
}

func TestClientCheckoutNewBranch(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
	}{
		{
			name:        "checkout new branch",
			wantCmdArgs: `path/to/git checkout -b trunk --track origin/trunk`,
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git checkout -b trunk --track origin/trunk`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			err := client.CheckoutNewBranch(context.Background(), "origin", "trunk")
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
		})
	}
}

func TestClientToplevelDir(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantDir       string
		wantErrorMsg  string
	}{
		{
			name:        "top level dir",
			cmdStdout:   "/path/to/repo",
			wantCmdArgs: `path/to/git rev-parse --show-toplevel`,
			wantDir:     "/path/to/repo",
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git rev-parse --show-toplevel`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			dir, err := client.ToplevelDir(context.Background())
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
			assert.Equal(t, tt.wantDir, dir)
		})
	}
}

func TestClientGitDir(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantDir       string
		wantErrorMsg  string
	}{
		{
			name:        "git dir",
			cmdStdout:   "/path/to/repo/.git",
			wantCmdArgs: `path/to/git rev-parse --git-dir`,
			wantDir:     "/path/to/repo/.git",
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git rev-parse --git-dir`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			dir, err := client.GitDir(context.Background())
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
			assert.Equal(t, tt.wantDir, dir)
		})
	}
}

func TestClientPathFromRoot(t *testing.T) {
	tests := []struct {
		name          string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
		wantDir       string
	}{
		{
			name:        "current path from root",
			cmdStdout:   "some/path/",
			wantCmdArgs: `path/to/git rev-parse --show-prefix`,
			wantDir:     "some/path",
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git rev-parse --show-prefix`,
			wantDir:       "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			dir := client.PathFromRoot(context.Background())
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			assert.Equal(t, tt.wantDir, dir)
		})
	}
}

func TestClientFetch(t *testing.T) {
	tests := []struct {
		name          string
		mods          []CommandModifier
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
	}{
		{
			name:        "fetch",
			wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential fetch origin trunk`,
		},
		{
			name:        "accepts command modifiers",
			mods:        []CommandModifier{WithRepoDir("/path/to/repo")},
			wantCmdArgs: `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential fetch origin trunk`,
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential fetch origin trunk`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			err := client.Fetch(context.Background(), "origin", "trunk", tt.mods...)
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
		})
	}
}

func TestClientPull(t *testing.T) {
	tests := []struct {
		name          string
		mods          []CommandModifier
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
	}{
		{
			name:        "pull",
			wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential pull --ff-only origin trunk`,
		},
		{
			name:        "accepts command modifiers",
			mods:        []CommandModifier{WithRepoDir("/path/to/repo")},
			wantCmdArgs: `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential pull --ff-only origin trunk`,
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential pull --ff-only origin trunk`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			err := client.Pull(context.Background(), "origin", "trunk", tt.mods...)
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
		})
	}
}

func TestClientPush(t *testing.T) {
	tests := []struct {
		name          string
		mods          []CommandModifier
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
	}{
		{
			name:        "push",
			wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential push --set-upstream origin trunk`,
		},
		{
			name:        "accepts command modifiers",
			mods:        []CommandModifier{WithRepoDir("/path/to/repo")},
			wantCmdArgs: `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential push --set-upstream origin trunk`,
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential push --set-upstream origin trunk`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			err := client.Push(context.Background(), "origin", "trunk", tt.mods...)
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
		})
	}
}

func TestClientClone(t *testing.T) {
	tests := []struct {
		name          string
		mods          []CommandModifier
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantTarget    string
		wantErrorMsg  string
	}{
		{
			name:        "clone",
			wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential clone github.com/cli/cli`,
			wantTarget:  "cli",
		},
		{
			name:        "accepts command modifiers",
			mods:        []CommandModifier{WithRepoDir("/path/to/repo")},
			wantCmdArgs: `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential clone github.com/cli/cli`,
			wantTarget:  "cli",
		},
		{
			name:          "git error",
			cmdExitStatus: 1,
			cmdStderr:     "git error message",
			wantCmdArgs:   `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential clone github.com/cli/cli`,
			wantErrorMsg:  "failed to run git: git error message",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				commandContext: cmdCtx,
			}
			target, err := client.Clone(context.Background(), "github.com/cli/cli", []string{}, tt.mods...)
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			if tt.wantErrorMsg == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantErrorMsg)
			}
			assert.Equal(t, tt.wantTarget, target)
		})
	}
}

func TestParseCloneArgs(t *testing.T) {
	type wanted struct {
		args []string
		dir  string
	}
	tests := []struct {
		name string
		args []string
		want wanted
	}{
		{
			name: "args and target",
			args: []string{"target_directory", "-o", "upstream", "--depth", "1"},
			want: wanted{
				args: []string{"-o", "upstream", "--depth", "1"},
				dir:  "target_directory",
			},
		},
		{
			name: "only args",
			args: []string{"-o", "upstream", "--depth", "1"},
			want: wanted{
				args: []string{"-o", "upstream", "--depth", "1"},
				dir:  "",
			},
		},
		{
			name: "only target",
			args: []string{"target_directory"},
			want: wanted{
				args: []string{},
				dir:  "target_directory",
			},
		},
		{
			name: "no args",
			args: []string{},
			want: wanted{
				args: []string{},
				dir:  "",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			args, dir := parseCloneArgs(tt.args)
			got := wanted{args: args, dir: dir}
			assert.Equal(t, got, tt.want)
		})
	}
}

func TestClientAddRemote(t *testing.T) {
	tests := []struct {
		title         string
		name          string
		url           string
		branches      []string
		dir           string
		cmdExitStatus int
		cmdStdout     string
		cmdStderr     string
		wantCmdArgs   string
		wantErrorMsg  string
	}{
		{
			title:       "fetch all",
			name:        "test",
			url:         "URL",
			dir:         "DIRECTORY",
			branches:    []string{},
			wantCmdArgs: `path/to/git -C DIRECTORY remote add test URL`,
		},
		{
			title:       "fetch specific branches only",
			name:        "test",
			url:         "URL",
			dir:         "DIRECTORY",
			branches:    []string{"trunk", "dev"},
			wantCmdArgs: `path/to/git -C DIRECTORY remote add -t trunk -t dev test URL`,
		},
	}
	for _, tt := range tests {
		t.Run(tt.title, func(t *testing.T) {
			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
			client := Client{
				GitPath:        "path/to/git",
				RepoDir:        tt.dir,
				commandContext: cmdCtx,
			}
			_, err := client.AddRemote(context.Background(), tt.name, tt.url, tt.branches)
			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
			assert.NoError(t, err)
		})
	}
}

func initRepo(t *testing.T, dir string) {
	errBuf := &bytes.Buffer{}
	inBuf := &bytes.Buffer{}
	outBuf := &bytes.Buffer{}
	client := Client{
		RepoDir: dir,
		Stderr:  errBuf,
		Stdin:   inBuf,
		Stdout:  outBuf,
	}
	cmd, err := client.Command(context.Background(), []string{"init", "--quiet"}...)
	assert.NoError(t, err)
	_, err = cmd.Output()
	assert.NoError(t, err)
}

func TestHelperProcess(t *testing.T) {
	if os.Getenv("GH_WANT_HELPER_PROCESS") != "1" {
		return
	}
	if err := func(args []string) error {
		fmt.Fprint(os.Stdout, os.Getenv("GH_HELPER_PROCESS_STDOUT"))
		exitStatus := os.Getenv("GH_HELPER_PROCESS_EXIT_STATUS")
		if exitStatus != "0" {
			return errors.New("error")
		}
		return nil
	}(os.Args[3:]); err != nil {
		fmt.Fprint(os.Stderr, os.Getenv("GH_HELPER_PROCESS_STDERR"))
		exitStatus := os.Getenv("GH_HELPER_PROCESS_EXIT_STATUS")
		i, err := strconv.Atoi(exitStatus)
		if err != nil {
			os.Exit(1)
		}
		os.Exit(i)
	}
	os.Exit(0)
}

func createCommandContext(t *testing.T, exitStatus int, stdout, stderr string) (*exec.Cmd, commandCtx) {
	cmd := exec.CommandContext(context.Background(), os.Args[0], "-test.run=TestHelperProcess", "--")
	cmd.Env = []string{
		"GH_WANT_HELPER_PROCESS=1",
		fmt.Sprintf("GH_HELPER_PROCESS_STDOUT=%s", stdout),
		fmt.Sprintf("GH_HELPER_PROCESS_STDERR=%s", stderr),
		fmt.Sprintf("GH_HELPER_PROCESS_EXIT_STATUS=%v", exitStatus),
	}
	return cmd, func(ctx context.Context, exe string, args ...string) *exec.Cmd {
		cmd.Args = append(cmd.Args, exe)
		cmd.Args = append(cmd.Args, args...)
		return cmd
	}
}
