package create

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"testing"

	"github.com/MakeNowJust/heredoc"
	"github.com/cli/cli/v2/git"
	"github.com/cli/cli/v2/internal/config"
	"github.com/cli/cli/v2/internal/gh"
	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/internal/prompter"
	"github.com/cli/cli/v2/internal/run"
	"github.com/cli/cli/v2/pkg/cmd/release/shared"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/httpmock"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/google/shlex"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func Test_NewCmdCreate(t *testing.T) {
	tempDir := t.TempDir()
	tf, err := os.CreateTemp(tempDir, "release-create")
	require.NoError(t, err)
	fmt.Fprint(tf, "MY NOTES")
	tf.Close()
	af1, err := os.Create(filepath.Join(tempDir, "windows.zip"))
	require.NoError(t, err)
	af1.Close()
	af2, err := os.Create(filepath.Join(tempDir, "linux.tgz"))
	require.NoError(t, err)
	af2.Close()

	tests := []struct {
		name    string
		args    string
		isTTY   bool
		stdin   string
		want    CreateOptions
		wantErr string
	}{
		{
			name:  "no arguments tty",
			args:  "",
			isTTY: true,
			want: CreateOptions{
				TagName:         "",
				Target:          "",
				Name:            "",
				Body:            "",
				BodyProvided:    false,
				Draft:           false,
				Prerelease:      false,
				RepoOverride:    "",
				Concurrency:     5,
				Assets:          []*shared.AssetForUpload(nil),
				VerifyTag:       false,
				FailOnNoCommits: false,
			},
		},
		{
			name:    "no arguments notty",
			args:    "",
			isTTY:   false,
			wantErr: "tag required when not running interactively",
		},
		{
			name:  "only tag name",
			args:  "v1.2.3",
			isTTY: true,
			want: CreateOptions{
				TagName:         "v1.2.3",
				Target:          "",
				Name:            "",
				Body:            "",
				BodyProvided:    false,
				Draft:           false,
				Prerelease:      false,
				RepoOverride:    "",
				Concurrency:     5,
				Assets:          []*shared.AssetForUpload(nil),
				VerifyTag:       false,
				FailOnNoCommits: false,
			},
		},
		{
			name:  "asset files",
			args:  fmt.Sprintf("v1.2.3 '%s' '%s#Linux build'", af1.Name(), af2.Name()),
			isTTY: true,
			want: CreateOptions{
				TagName:      "v1.2.3",
				Target:       "",
				Name:         "",
				Body:         "",
				BodyProvided: false,
				Draft:        false,
				Prerelease:   false,
				RepoOverride: "",
				Concurrency:  5,
				Assets: []*shared.AssetForUpload{
					{
						Name:  "windows.zip",
						Label: "",
					},
					{
						Name:  "linux.tgz",
						Label: "Linux build",
					},
				},
			},
		},
		{
			name:  "provide title and body",
			args:  "v1.2.3 -t mytitle -n mynotes",
			isTTY: true,
			want: CreateOptions{
				TagName:      "v1.2.3",
				Target:       "",
				Name:         "mytitle",
				Body:         "mynotes",
				BodyProvided: true,
				Draft:        false,
				Prerelease:   false,
				RepoOverride: "",
				Concurrency:  5,
				Assets:       []*shared.AssetForUpload(nil),
			},
		},
		{
			name:  "notes from file",
			args:  fmt.Sprintf(`v1.2.3 -F '%s'`, tf.Name()),
			isTTY: true,
			want: CreateOptions{
				TagName:      "v1.2.3",
				Target:       "",
				Name:         "",
				Body:         "MY NOTES",
				BodyProvided: true,
				Draft:        false,
				Prerelease:   false,
				RepoOverride: "",
				Concurrency:  5,
				Assets:       []*shared.AssetForUpload(nil),
			},
		},
		{
			name:  "notes from stdin",
			args:  "v1.2.3 -F -",
			isTTY: true,
			stdin: "MY NOTES",
			want: CreateOptions{
				TagName:      "v1.2.3",
				Target:       "",
				Name:         "",
				Body:         "MY NOTES",
				BodyProvided: true,
				Draft:        false,
				Prerelease:   false,
				RepoOverride: "",
				Concurrency:  5,
				Assets:       []*shared.AssetForUpload(nil),
			},
		},
		{
			name:  "set draft and prerelease",
			args:  "v1.2.3 -d -p",
			isTTY: true,
			want: CreateOptions{
				TagName:      "v1.2.3",
				Target:       "",
				Name:         "",
				Body:         "",
				BodyProvided: false,
				Draft:        true,
				Prerelease:   true,
				RepoOverride: "",
				Concurrency:  5,
				Assets:       []*shared.AssetForUpload(nil),
			},
		},
		{
			name:  "discussion category",
			args:  "v1.2.3 --discussion-category 'General'",
			isTTY: true,
			want: CreateOptions{
				TagName:            "v1.2.3",
				Target:             "",
				Name:               "",
				Body:               "",
				BodyProvided:       false,
				Draft:              false,
				Prerelease:         false,
				RepoOverride:       "",
				Concurrency:        5,
				Assets:             []*shared.AssetForUpload(nil),
				DiscussionCategory: "General",
			},
		},
		{
			name:    "discussion category for draft release",
			args:    "v1.2.3 -d --discussion-category 'General'",
			isTTY:   true,
			wantErr: "discussions for draft releases not supported",
		},
		{
			name:  "generate release notes",
			args:  "v1.2.3 --generate-notes",
			isTTY: true,
			want: CreateOptions{
				TagName:       "v1.2.3",
				Target:        "",
				Name:          "",
				Body:          "",
				BodyProvided:  true,
				Draft:         false,
				Prerelease:    false,
				RepoOverride:  "",
				Concurrency:   5,
				Assets:        []*shared.AssetForUpload(nil),
				GenerateNotes: true,
			},
		},
		{
			name:  "generate release notes with notes tag",
			args:  "v1.2.3 --generate-notes --notes-start-tag v1.1.0",
			isTTY: true,
			want: CreateOptions{
				TagName:       "v1.2.3",
				Target:        "",
				Name:          "",
				Body:          "",
				BodyProvided:  true,
				Draft:         false,
				Prerelease:    false,
				RepoOverride:  "",
				Concurrency:   5,
				Assets:        []*shared.AssetForUpload(nil),
				GenerateNotes: true,
				NotesStartTag: "v1.1.0",
			},
		},
		{
			name:  "notes tag",
			args:  "--notes-start-tag v1.1.0",
			isTTY: true,
			want: CreateOptions{
				TagName:       "",
				Target:        "",
				Name:          "",
				Body:          "",
				BodyProvided:  false,
				Draft:         false,
				Prerelease:    false,
				RepoOverride:  "",
				Concurrency:   5,
				Assets:        []*shared.AssetForUpload(nil),
				GenerateNotes: false,
				NotesStartTag: "v1.1.0",
			},
		},
		{
			name:  "latest",
			args:  "--latest v1.1.0",
			isTTY: false,
			want: CreateOptions{
				TagName:       "v1.1.0",
				Target:        "",
				Name:          "",
				Body:          "",
				BodyProvided:  false,
				Draft:         false,
				Prerelease:    false,
				IsLatest:      boolPtr(true),
				RepoOverride:  "",
				Concurrency:   5,
				Assets:        []*shared.AssetForUpload(nil),
				GenerateNotes: false,
				NotesStartTag: "",
			},
		},
		{
			name:  "not latest",
			args:  "--latest=false v1.1.0",
			isTTY: false,
			want: CreateOptions{
				TagName:       "v1.1.0",
				Target:        "",
				Name:          "",
				Body:          "",
				BodyProvided:  false,
				Draft:         false,
				Prerelease:    false,
				IsLatest:      boolPtr(false),
				RepoOverride:  "",
				Concurrency:   5,
				Assets:        []*shared.AssetForUpload(nil),
				GenerateNotes: false,
				NotesStartTag: "",
			},
		},
		{
			name:  "with verify-tag",
			args:  "v1.1.0 --verify-tag",
			isTTY: true,
			want: CreateOptions{
				TagName:       "v1.1.0",
				Target:        "",
				Name:          "",
				Body:          "",
				BodyProvided:  false,
				Draft:         false,
				Prerelease:    false,
				RepoOverride:  "",
				Concurrency:   5,
				Assets:        []*shared.AssetForUpload(nil),
				GenerateNotes: false,
				VerifyTag:     true,
			},
		},
		{
			name:  "with --notes-from-tag",
			args:  "v1.2.3 --notes-from-tag",
			isTTY: false,
			want: CreateOptions{
				TagName:      "v1.2.3",
				BodyProvided: true,
				Concurrency:  5,
				Assets:       []*shared.AssetForUpload(nil),
				NotesFromTag: true,
			},
		},
		{
			name:    "with --notes-from-tag and --generate-notes",
			args:    "v1.2.3 --notes-from-tag --generate-notes",
			isTTY:   false,
			wantErr: "using `--notes-from-tag` with `--generate-notes` or `--notes-start-tag` is not supported",
		},
		{
			name:    "with --notes-from-tag and --notes-start-tag",
			args:    "v1.2.3 --notes-from-tag --notes-start-tag v1.2.3",
			isTTY:   false,
			wantErr: "using `--notes-from-tag` with `--generate-notes` or `--notes-start-tag` is not supported",
		},
		{
			name:  "with --fail-on-no-commits",
			args:  "v1.2.3 --fail-on-no-commits",
			isTTY: false,
			want: CreateOptions{
				TagName:         "v1.2.3",
				BodyProvided:    false,
				Concurrency:     5,
				Assets:          []*shared.AssetForUpload(nil),
				NotesFromTag:    false,
				FailOnNoCommits: true,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, stdin, _, _ := iostreams.Test()
			if tt.stdin == "" {
				ios.SetStdinTTY(tt.isTTY)
			} else {
				ios.SetStdinTTY(false)
				fmt.Fprint(stdin, tt.stdin)
			}
			ios.SetStdoutTTY(tt.isTTY)
			ios.SetStderrTTY(tt.isTTY)

			f := &cmdutil.Factory{
				IOStreams: ios,
			}

			var opts *CreateOptions
			cmd := NewCmdCreate(f, func(o *CreateOptions) error {
				opts = o
				return nil
			})
			cmd.PersistentFlags().StringP("repo", "R", "", "")

			argv, err := shlex.Split(tt.args)
			require.NoError(t, err)
			cmd.SetArgs(argv)

			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(io.Discard)
			cmd.SetErr(io.Discard)

			_, err = cmd.ExecuteC()
			if tt.wantErr != "" {
				require.EqualError(t, err, tt.wantErr)
				return
			} else {
				require.NoError(t, err)
			}

			assert.Equal(t, tt.want.TagName, opts.TagName)
			assert.Equal(t, tt.want.Target, opts.Target)
			assert.Equal(t, tt.want.Name, opts.Name)
			assert.Equal(t, tt.want.Body, opts.Body)
			assert.Equal(t, tt.want.BodyProvided, opts.BodyProvided)
			assert.Equal(t, tt.want.Draft, opts.Draft)
			assert.Equal(t, tt.want.Prerelease, opts.Prerelease)
			assert.Equal(t, tt.want.Concurrency, opts.Concurrency)
			assert.Equal(t, tt.want.RepoOverride, opts.RepoOverride)
			assert.Equal(t, tt.want.DiscussionCategory, opts.DiscussionCategory)
			assert.Equal(t, tt.want.GenerateNotes, opts.GenerateNotes)
			assert.Equal(t, tt.want.NotesStartTag, opts.NotesStartTag)
			assert.Equal(t, tt.want.IsLatest, opts.IsLatest)
			assert.Equal(t, tt.want.VerifyTag, opts.VerifyTag)
			assert.Equal(t, tt.want.NotesFromTag, opts.NotesFromTag)
			assert.Equal(t, tt.want.FailOnNoCommits, opts.FailOnNoCommits)

			require.Equal(t, len(tt.want.Assets), len(opts.Assets))
			for i := range tt.want.Assets {
				assert.Equal(t, tt.want.Assets[i].Name, opts.Assets[i].Name)
				assert.Equal(t, tt.want.Assets[i].Label, opts.Assets[i].Label)
			}
		})
	}
}

func Test_createRun(t *testing.T) {
	const contentCmd = `git tag --list .* --format=%\(contents\)`
	const signatureCmd = `git tag --list .* --format=%\(contents:signature\)`

	defaultRunStubs := func(rs *run.CommandStubber) {
		rs.Register(contentCmd, 0, "")
		rs.Register(signatureCmd, 0, "")
	}

	tests := []struct {
		name       string
		isTTY      bool
		opts       CreateOptions
		httpStubs  func(t *testing.T, reg *httpmock.Registry)
		runStubs   func(rs *run.CommandStubber)
		wantErr    string
		wantStdout string
		wantStderr string
	}{
		{
			name:  "create a release",
			isTTY: true,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Name:         "The Big 1.2",
				Body:         "* Fixed bugs",
				BodyProvided: true,
				Target:       "",
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":   "v1.2.3",
						"name":       "The Big 1.2",
						"body":       "* Fixed bugs",
						"draft":      false,
						"prerelease": false,
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantStderr: ``,
		},
		{
			name:  "create a release if there are new commits and the last release does not exist",
			isTTY: true,
			opts: CreateOptions{
				TagName:         "v1.2.3",
				Name:            "The Big 1.2",
				Body:            "* Fixed bugs",
				BodyProvided:    true,
				Target:          "",
				FailOnNoCommits: true,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(404, `{
					"message": "Not Found",
					"documentation_url": "https://docs.github.com/rest/releases/releases#get-the-latest-release",
					"status": "404"
				}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":   "v1.2.3",
						"name":       "The Big 1.2",
						"body":       "* Fixed bugs",
						"draft":      false,
						"prerelease": false,
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantStderr: ``,
		},
		{
			name:  "create a release if there are new commits and the last release exists",
			isTTY: true,
			opts: CreateOptions{
				TagName:         "v1.2.3",
				Name:            "The Big 1.2",
				Body:            "* Fixed bugs",
				BodyProvided:    true,
				Target:          "",
				FailOnNoCommits: true,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(200, `{
					"tag_name": "v1.2.2"
				}`))
				reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/compare/v1.2.2...HEAD"), httpmock.StatusStringResponse(200, `{
						"status": "ahead"
				}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":   "v1.2.3",
						"name":       "The Big 1.2",
						"body":       "* Fixed bugs",
						"draft":      false,
						"prerelease": false,
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantStderr: ``,
		},
		{
			name:  "create a release if there are no new commits but the last release exists",
			isTTY: true,
			opts: CreateOptions{
				TagName:         "v1.2.3",
				Name:            "The Big 1.2",
				Body:            "* Fixed bugs",
				BodyProvided:    true,
				Target:          "",
				FailOnNoCommits: true,
			},
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(200, `{
					"tag_name": "v1.2.2"
				}`))
				reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/compare/v1.2.2...HEAD"), httpmock.StatusStringResponse(200, `{
					"status": "identical"
				}`))
			},
			wantErr:    "no new commits since the last release",
			wantStdout: "",
			wantStderr: ``,
		},
		{
			name:  "with discussion category",
			isTTY: true,
			opts: CreateOptions{
				TagName:            "v1.2.3",
				Name:               "The Big 1.2",
				Body:               "* Fixed bugs",
				BodyProvided:       true,
				Target:             "",
				DiscussionCategory: "General",
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":                 "v1.2.3",
						"name":                     "The Big 1.2",
						"body":                     "* Fixed bugs",
						"draft":                    false,
						"prerelease":               false,
						"discussion_category_name": "General",
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantStderr: ``,
		},
		{
			name:  "with target commitish",
			isTTY: true,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Name:         "",
				Body:         "",
				BodyProvided: true,
				Target:       "main",
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":         "v1.2.3",
						"draft":            false,
						"prerelease":       false,
						"target_commitish": "main",
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantStderr: ``,
		},
		{
			name:  "as draft",
			isTTY: true,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Name:         "",
				Body:         "",
				BodyProvided: true,
				Draft:        true,
				Target:       "",
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":   "v1.2.3",
						"draft":      true,
						"prerelease": false,
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantStderr: ``,
		},
		{
			name:  "with latest",
			isTTY: false,
			opts: CreateOptions{
				TagName:       "v1.2.3",
				Name:          "",
				Body:          "",
				Target:        "",
				IsLatest:      boolPtr(true),
				BodyProvided:  true,
				GenerateNotes: false,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":    "v1.2.3",
						"draft":       false,
						"prerelease":  false,
						"make_latest": "true",
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantErr:    "",
		},
		{
			name:  "with generate notes",
			isTTY: true,
			opts: CreateOptions{
				TagName:       "v1.2.3",
				Name:          "",
				Body:          "",
				Target:        "",
				BodyProvided:  true,
				GenerateNotes: true,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":               "v1.2.3",
						"draft":                  false,
						"prerelease":             false,
						"generate_release_notes": true,
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantErr:    "",
		},
		{
			name:  "with generate notes and notes tag",
			isTTY: true,
			opts: CreateOptions{
				TagName:       "v1.2.3",
				Name:          "",
				Body:          "",
				Target:        "",
				BodyProvided:  true,
				GenerateNotes: true,
				NotesStartTag: "v1.1.0",
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.RESTPayload(200, `{
						"name": "generated name",
						"body": "generated body"
				}`, func(params map[string]interface{}) {
						assert.Equal(t, map[string]interface{}{
							"tag_name":          "v1.2.3",
							"previous_tag_name": "v1.1.0",
						}, params)
					}))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":   "v1.2.3",
						"draft":      false,
						"prerelease": false,
						"body":       "generated body",
						"name":       "generated name",
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantErr:    "",
		},
		{
			name:  "with generate notes and notes tag and body and name",
			isTTY: true,
			opts: CreateOptions{
				TagName:       "v1.2.3",
				Name:          "name",
				Body:          "body",
				Target:        "",
				BodyProvided:  true,
				GenerateNotes: true,
				NotesStartTag: "v1.1.0",
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.RESTPayload(200, `{
						"name": "generated name",
						"body": "generated body"
				}`, func(params map[string]interface{}) {
						assert.Equal(t, map[string]interface{}{
							"tag_name":          "v1.2.3",
							"previous_tag_name": "v1.1.0",
						}, params)
					}))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":   "v1.2.3",
						"draft":      false,
						"prerelease": false,
						"body":       "body\ngenerated body",
						"name":       "name",
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantErr:    "",
		},
		{
			name:  "publish after uploading files",
			isTTY: true,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Name:         "",
				Body:         "",
				BodyProvided: true,
				Draft:        false,
				Target:       "",
				Assets: []*shared.AssetForUpload{
					{
						Name: "ball.tgz",
						Open: func() (io.ReadCloser, error) {
							return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
						},
					},
				},
				Concurrency: 1,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":   "v1.2.3",
						"draft":      true,
						"prerelease": false,
					}, params)
				}))
				reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) {
					q := req.URL.Query()
					assert.Equal(t, "ball.tgz", q.Get("name"))
					assert.Equal(t, "", q.Get("label"))
					return &http.Response{
						StatusCode: 201,
						Request:    req,
						Body:       io.NopCloser(bytes.NewBufferString(`{}`)),
						Header: map[string][]string{
							"Content-Type": {"application/json"},
						},
					}, nil
				})
				reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"draft": false,
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n",
			wantStderr: ``,
		},
		{
			name:  "publish after uploading files, but do not mark as latest",
			isTTY: true,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Name:         "",
				Body:         "",
				BodyProvided: true,
				Draft:        false,
				IsLatest:     boolPtr(false),
				Target:       "",
				Assets: []*shared.AssetForUpload{
					{
						Name: "ball.tgz",
						Open: func() (io.ReadCloser, error) {
							return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
						},
					},
				},
				Concurrency: 1,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":    "v1.2.3",
						"draft":       true,
						"prerelease":  false,
						"make_latest": "false",
					}, params)
				}))
				reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) {
					q := req.URL.Query()
					assert.Equal(t, "ball.tgz", q.Get("name"))
					assert.Equal(t, "", q.Get("label"))
					return &http.Response{
						StatusCode: 201,
						Request:    req,
						Body:       io.NopCloser(bytes.NewBufferString(`{}`)),
						Header: map[string][]string{
							"Content-Type": {"application/json"},
						},
					}, nil
				})
				reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"draft":       false,
						"make_latest": "false",
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n",
			wantStderr: ``,
		},
		{
			name:  "upload files but release already exists",
			isTTY: true,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Name:         "",
				Body:         "",
				BodyProvided: true,
				Draft:        false,
				Target:       "",
				Assets: []*shared.AssetForUpload{
					{
						Name: "ball.tgz",
						Open: func() (io.ReadCloser, error) {
							return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
						},
					},
				},
				Concurrency: 1,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(200, ``))
			},
			wantStdout: ``,
			wantStderr: ``,
			wantErr:    `a release with the same tag name already exists: v1.2.3`,
		},
		{
			name:  "clean up draft after uploading files fails",
			isTTY: false,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Name:         "",
				Body:         "",
				BodyProvided: true,
				Draft:        false,
				Target:       "",
				Assets: []*shared.AssetForUpload{
					{
						Name: "ball.tgz",
						Open: func() (io.ReadCloser, error) {
							return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
						},
					},
				},
				Concurrency: 1,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`))
				reg.Register(httpmock.REST("POST", "assets/upload"), httpmock.StatusStringResponse(422, `{}`))
				reg.Register(httpmock.REST("DELETE", "releases/123"), httpmock.StatusStringResponse(204, ``))
			},
			wantStdout: ``,
			wantStderr: ``,
			wantErr:    `HTTP 422 (https://api.github.com/assets/upload?label=&name=ball.tgz)`,
		},
		{
			name:  "clean up draft after publishing fails",
			isTTY: false,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Name:         "",
				Body:         "",
				BodyProvided: true,
				Draft:        false,
				Target:       "",
				Assets: []*shared.AssetForUpload{
					{
						Name: "ball.tgz",
						Open: func() (io.ReadCloser, error) {
							return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
						},
					},
				},
				Concurrency: 1,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`))
				reg.Register(httpmock.REST("POST", "assets/upload"), httpmock.StatusStringResponse(201, `{}`))
				reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.StatusStringResponse(500, `{}`))
				reg.Register(httpmock.REST("DELETE", "releases/123"), httpmock.StatusStringResponse(204, ``))
			},
			wantStdout: ``,
			wantStderr: ``,
			wantErr:    `HTTP 500 (https://api.github.com/releases/123)`,
		},
		{
			name:  "upload files but release already exists",
			isTTY: true,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Name:         "",
				Body:         "",
				BodyProvided: true,
				Draft:        false,
				Target:       "",
				Assets: []*shared.AssetForUpload{
					{
						Name: "ball.tgz",
						Open: func() (io.ReadCloser, error) {
							return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
						},
					},
				},
				Concurrency: 1,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(200, ``))
			},
			wantStdout: ``,
			wantStderr: ``,
			wantErr:    `a release with the same tag name already exists: v1.2.3`,
		},
		{
			name:  "upload files and create discussion",
			isTTY: true,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Name:         "",
				Body:         "",
				BodyProvided: true,
				Draft:        false,
				Target:       "",
				Assets: []*shared.AssetForUpload{
					{
						Name: "ball.tgz",
						Open: func() (io.ReadCloser, error) {
							return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
						},
					},
				},
				DiscussionCategory: "general",
				Concurrency:        1,
			},
			runStubs: defaultRunStubs,
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"tag_name":                 "v1.2.3",
						"draft":                    true,
						"prerelease":               false,
						"discussion_category_name": "general",
					}, params)
				}))
				reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) {
					q := req.URL.Query()
					assert.Equal(t, "ball.tgz", q.Get("name"))
					assert.Equal(t, "", q.Get("label"))
					return &http.Response{
						StatusCode: 201,
						Request:    req,
						Body:       io.NopCloser(bytes.NewBufferString(`{}`)),
						Header: map[string][]string{
							"Content-Type": {"application/json"},
						},
					}, nil
				})
				reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
				}`, func(params map[string]interface{}) {
					assert.Equal(t, map[string]interface{}{
						"draft":                    false,
						"discussion_category_name": "general",
					}, params)
				}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n",
			wantStderr: ``,
		},
		{
			name:  "with generate notes from tag",
			isTTY: false,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				BodyProvided: true,
				Concurrency:  5,
				Assets:       []*shared.AssetForUpload(nil),
				NotesFromTag: true,
			},
			runStubs: func(rs *run.CommandStubber) {
				rs.Register(contentCmd, 0, "some tag message")
				rs.Register(signatureCmd, 0, "")
			},
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("RepositoryFindRef"),
					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`),
				)
				reg.Register(
					httpmock.REST("POST", "repos/OWNER/REPO/releases"),
					httpmock.RESTPayload(201, `{
						"url": "https://api.github.com/releases/123",
						"upload_url": "https://api.github.com/assets/upload",
						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
					}`, func(payload map[string]interface{}) {
						assert.Equal(t, map[string]interface{}{
							"tag_name":   "v1.2.3",
							"draft":      false,
							"prerelease": false,
							"body":       "some tag message",
						}, payload)
					}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantStderr: "",
		},
		{
			name:  "with generate notes from tag and notes provided",
			isTTY: false,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				Body:         "some notes here",
				BodyProvided: true,
				Concurrency:  5,
				Assets:       []*shared.AssetForUpload(nil),
				NotesFromTag: true,
			},
			runStubs: func(rs *run.CommandStubber) {
				rs.Register(contentCmd, 0, "some tag message")
				rs.Register(signatureCmd, 0, "")
			},
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("RepositoryFindRef"),
					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`),
				)
				reg.Register(
					httpmock.REST("POST", "repos/OWNER/REPO/releases"),
					httpmock.RESTPayload(201, `{
						"url": "https://api.github.com/releases/123",
						"upload_url": "https://api.github.com/assets/upload",
						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
					}`, func(payload map[string]interface{}) {
						assert.Equal(t, map[string]interface{}{
							"tag_name":   "v1.2.3",
							"draft":      false,
							"prerelease": false,
							"body":       "some notes here\nsome tag message",
						}, payload)
					}))
			},
			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
			wantStderr: "",
		},
		{
			name:  "with generate notes from tag and tag does not exist",
			isTTY: false,
			opts: CreateOptions{
				TagName:      "v1.2.3",
				BodyProvided: true,
				Concurrency:  5,
				Assets:       []*shared.AssetForUpload(nil),
				NotesFromTag: true,
			},
			runStubs: defaultRunStubs,
			wantErr:  "cannot generate release notes from tag v1.2.3 as it does not exist locally",
		},
		{
			name:  "API returns 404, OAuth token has no workflow scope",
			isTTY: false,
			opts: CreateOptions{
				TagName: "Does not matter",
			},
			runStubs: func(rs *run.CommandStubber) {
				rs.Register(contentCmd, 0, "some tag message")
				rs.Register(signatureCmd, 0, "")
			},
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("RepositoryFindRef"),
					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`),
				)
				reg.Register(
					httpmock.REST("POST", "repos/OWNER/REPO/releases"),
					httpmock.StatusScopesResponder(404, `repo,read:org`))
			},
			wantStderr: heredoc.Doc(`
				! Failed to create release, "workflow" scope may be required.
				To request it, run:
				gh auth refresh -h github.com -s workflow
			`),
			wantErr: cmdutil.SilentError.Error(),
		},
		{
			name:  "API returns 404, OAuth token has workflow scope",
			isTTY: false,
			opts: CreateOptions{
				TagName: "Does not matter",
			},
			runStubs: func(rs *run.CommandStubber) {
				rs.Register(contentCmd, 0, "some tag message")
				rs.Register(signatureCmd, 0, "")
			},
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("RepositoryFindRef"),
					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`),
				)
				reg.Register(
					httpmock.REST("POST", "repos/OWNER/REPO/releases"),
					httpmock.StatusScopesResponder(404, `repo,read:org,workflow`))
			},
			wantErr: "HTTP 404 (https://api.github.com/repos/OWNER/REPO/releases)",
		},
		{
			name:  "API returns 404, not an OAuth token",
			isTTY: false,
			opts: CreateOptions{
				TagName: "Does not matter",
			},
			runStubs: func(rs *run.CommandStubber) {
				rs.Register(contentCmd, 0, "some tag message")
				rs.Register(signatureCmd, 0, "")
			},
			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("RepositoryFindRef"),
					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`),
				)
				reg.Register(
					httpmock.REST("POST", "repos/OWNER/REPO/releases"),
					httpmock.StatusStringResponse(404, `HTTP 404 (https://api.github.com/repos/OWNER/REPO/releases)`))
			},
			wantErr: "HTTP 404 (https://api.github.com/repos/OWNER/REPO/releases)",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, _, stdout, stderr := iostreams.Test()
			ios.SetStdoutTTY(tt.isTTY)
			ios.SetStdinTTY(tt.isTTY)
			ios.SetStderrTTY(tt.isTTY)

			fakeHTTP := &httpmock.Registry{}
			if tt.httpStubs != nil {
				tt.httpStubs(t, fakeHTTP)
			}
			defer fakeHTTP.Verify(t)

			tt.opts.IO = ios
			tt.opts.HttpClient = func() (*http.Client, error) {
				return &http.Client{Transport: fakeHTTP}, nil
			}
			tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
				return ghrepo.FromFullName("OWNER/REPO")
			}

			tt.opts.GitClient = &git.Client{GitPath: "some/path/git"}

			rs, teardown := run.Stub()
			defer teardown(t)
			if tt.runStubs != nil {
				tt.runStubs(rs)
			}

			err := createRun(&tt.opts)
			if tt.wantErr != "" {
				require.EqualError(t, err, tt.wantErr)
			} else {
				require.NoError(t, err)
			}

			assert.Equal(t, tt.wantStdout, stdout.String())
			assert.Equal(t, tt.wantStderr, stderr.String())
		})
	}
}

func Test_createRun_interactive(t *testing.T) {
	const contentCmd = `git tag --list .* --format=%\(contents\)`
	const signatureCmd = `git tag --list .* --format=%\(contents:signature\)`

	defaultRunStubs := func(rs *run.CommandStubber) {
		rs.Register(contentCmd, 1, "")
	}

	tests := []struct {
		name          string
		httpStubs     func(*httpmock.Registry)
		prompterStubs func(*testing.T, *prompter.MockPrompter)
		runStubs      func(*run.CommandStubber)
		opts          *CreateOptions
		wantParams    map[string]interface{}
		wantOut       string
		wantErr       string
	}{
		{
			name: "create a release from existing tag",
			opts: &CreateOptions{},
			prompterStubs: func(t *testing.T, pm *prompter.MockPrompter) {
				pm.RegisterSelect("Choose a tag",
					[]string{"v1.2.3", "v1.2.2", "v1.0.0", "v0.1.2", "Create a new tag"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "v1.2.3")
					})
				pm.RegisterSelect("Release notes",
					[]string{"Write my own", "Write using generated notes as template", "Leave blank"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Leave blank")
					})
				pm.RegisterSelect("Submit?",
					[]string{"Publish release", "Save as draft", "Cancel"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Publish release")
					})
				pm.RegisterInput("Title (optional)", func(_, d string) (string, error) {
					return d, nil
				})
				pm.RegisterConfirm("Is this a prerelease?", func(_ string, _ bool) (bool, error) {
					return false, nil
				})
			},
			runStubs: defaultRunStubs,
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/tags"), httpmock.StatusStringResponse(200, `[
					{ "name": "v1.2.3" }, { "name": "v1.2.2" }, { "name": "v1.0.0" }, { "name": "v0.1.2" }
				]`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.StatusStringResponse(200, `{
						"name": "generated name",
						"body": "generated body"
					}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`))
			},
			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
		},
		{
			name: "create a release from new tag",
			opts: &CreateOptions{},
			prompterStubs: func(t *testing.T, pm *prompter.MockPrompter) {
				pm.RegisterSelect("Choose a tag",
					[]string{"v1.2.2", "v1.0.0", "v0.1.2", "Create a new tag"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Create a new tag")
					})
				pm.RegisterSelect("Release notes",
					[]string{"Write my own", "Write using generated notes as template", "Leave blank"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Leave blank")
					})
				pm.RegisterSelect("Submit?",
					[]string{"Publish release", "Save as draft", "Cancel"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Publish release")
					})
				pm.RegisterInput("Tag name", func(_, d string) (string, error) {
					return "v1.2.3", nil
				})
				pm.RegisterInput("Title (optional)", func(_, d string) (string, error) {
					return d, nil
				})
				pm.RegisterConfirm("Is this a prerelease?", func(_ string, _ bool) (bool, error) {
					return false, nil
				})
			},
			runStubs: defaultRunStubs,
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/tags"), httpmock.StatusStringResponse(200, `[
					{ "name": "v1.2.2" }, { "name": "v1.0.0" }, { "name": "v0.1.2" }
				]`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.StatusStringResponse(200, `{
						"name": "generated name",
						"body": "generated body"
					}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`))
			},
			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
		},
		{
			name: "create a release from new tag (with leading space)",
			opts: &CreateOptions{},
			prompterStubs: func(t *testing.T, pm *prompter.MockPrompter) {
				pm.RegisterSelect("Choose a tag",
					[]string{"v1.2.2", "v1.0.0", "v0.1.2", "Create a new tag"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Create a new tag")
					})
				pm.RegisterSelect("Release notes",
					[]string{"Write my own", "Write using generated notes as template", "Leave blank"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Leave blank")
					})
				pm.RegisterSelect("Submit?",
					[]string{"Publish release", "Save as draft", "Cancel"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Publish release")
					})
				pm.RegisterInput("Tag name", func(_, d string) (string, error) {
					return "  v1.2.3", nil
				})
				pm.RegisterInput("Title (optional)", func(_, d string) (string, error) {
					return d, nil
				})
				pm.RegisterConfirm("Is this a prerelease?", func(_ string, _ bool) (bool, error) {
					return false, nil
				})
			},
			runStubs: defaultRunStubs,
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/tags"), httpmock.StatusStringResponse(200, `[
					{ "name": "v1.2.2" }, { "name": "v1.0.0" }, { "name": "v0.1.2" }
				]`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.StatusStringResponse(200, `{
						"name": "generated name",
						"body": "generated body"
					}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`))
			},
			wantParams: map[string]interface{}{
				"draft":      false,
				"name":       "generated name",
				"prerelease": false,
				"tag_name":   "v1.2.3",
			},
			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
		},
		{
			name: "create a release using generated notes",
			opts: &CreateOptions{
				TagName: "v1.2.3",
			},
			prompterStubs: func(t *testing.T, pm *prompter.MockPrompter) {
				pm.RegisterSelect("Release notes",
					[]string{"Write my own", "Write using generated notes as template", "Leave blank"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Write using generated notes as template")
					})
				pm.RegisterSelect("Submit?",
					[]string{"Publish release", "Save as draft", "Cancel"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Publish release")
					})
				pm.RegisterInput("Title (optional)", func(_, d string) (string, error) {
					return d, nil
				})
				pm.RegisterConfirm("Is this a prerelease?", func(_ string, _ bool) (bool, error) {
					return false, nil
				})
			},
			runStubs: defaultRunStubs,
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.StatusStringResponse(200, `{
						"name": "generated name",
						"body": "generated body"
					}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
					httpmock.StatusStringResponse(201, `{
						"url": "https://api.github.com/releases/123",
						"upload_url": "https://api.github.com/assets/upload",
						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
					}`))
			},
			wantParams: map[string]interface{}{
				"body":       "generated body",
				"draft":      false,
				"name":       "generated name",
				"prerelease": false,
				"tag_name":   "v1.2.3",
			},
			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
		},
		{
			name: "create a release using commit log as notes",
			opts: &CreateOptions{
				TagName: "v1.2.3",
			},
			prompterStubs: func(t *testing.T, pm *prompter.MockPrompter) {
				pm.RegisterSelect("Release notes",
					[]string{"Write my own", "Write using commit log as template", "Leave blank"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Write using commit log as template")
					})
				pm.RegisterSelect("Submit?",
					[]string{"Publish release", "Save as draft", "Cancel"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Publish release")
					})
				pm.RegisterInput("Title (optional)", func(_, d string) (string, error) {
					return d, nil
				})
				pm.RegisterConfirm("Is this a prerelease?", func(_ string, _ bool) (bool, error) {
					return false, nil
				})
			},
			runStubs: func(rs *run.CommandStubber) {
				defaultRunStubs(rs)
				rs.Register(`git describe --tags --abbrev=0 HEAD\^`, 0, "v1.2.2\n")
				rs.Register(`git .+log .+v1\.2\.2\.\.HEAD$`, 0, "commit subject\n\ncommit body\n")
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.StatusStringResponse(404, `{}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
					httpmock.StatusStringResponse(201, `{
						"url": "https://api.github.com/releases/123",
						"upload_url": "https://api.github.com/assets/upload",
						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
					}`))
			},
			wantParams: map[string]interface{}{
				"body":       "* commit subject\n\n  commit body\n  ",
				"draft":      false,
				"prerelease": false,
				"tag_name":   "v1.2.3",
			},
			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
		},
		{
			name: "create using annotated tag as notes",
			opts: &CreateOptions{
				TagName: "v1.2.3",
			},
			prompterStubs: func(t *testing.T, pm *prompter.MockPrompter) {
				pm.RegisterSelect("Release notes",
					[]string{"Write my own", "Write using git tag message as template", "Leave blank"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Write using git tag message as template")
					})
				pm.RegisterSelect("Submit?",
					[]string{"Publish release", "Save as draft", "Cancel"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Publish release")
					})
				pm.RegisterInput("Title (optional)", func(_, d string) (string, error) {
					return d, nil
				})
				pm.RegisterConfirm("Is this a prerelease?", func(_ string, _ bool) (bool, error) {
					return false, nil
				})
			},
			runStubs: func(rs *run.CommandStubber) {
				rs.Register(contentCmd, 0, "hello from annotated tag")
				rs.Register(signatureCmd, 0, "")
				rs.Register(`git describe --tags --abbrev=0 v1\.2\.3\^`, 1, "")
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.GraphQL("RepositoryFindRef"),
					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.StatusStringResponse(404, `{}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
					httpmock.StatusStringResponse(201, `{
						"url": "https://api.github.com/releases/123",
						"upload_url": "https://api.github.com/assets/upload",
						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
					}`))
			},
			wantParams: map[string]interface{}{
				"body":       "hello from annotated tag",
				"draft":      false,
				"prerelease": false,
				"tag_name":   "v1.2.3",
			},
			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
		},
		{
			name: "error when unpublished local tag and target not specified",
			opts: &CreateOptions{
				TagName: "v1.2.3",
			},
			runStubs: func(rs *run.CommandStubber) {
				rs.Register(contentCmd, 0, "tag exists")
				rs.Register(signatureCmd, 0, "")
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.GraphQL("RepositoryFindRef"),
					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": ""}}}}`))
			},
			wantErr: "tag v1.2.3 exists locally but has not been pushed to OWNER/REPO, please push it before continuing or specify the `--target` flag to create a new tag",
		},
		{
			name: "create a release when unpublished local tag and target specified",
			opts: &CreateOptions{
				TagName: "v1.2.3",
				Target:  "main",
			},
			prompterStubs: func(t *testing.T, pm *prompter.MockPrompter) {
				pm.RegisterSelect("Release notes",
					[]string{"Write my own", "Write using generated notes as template", "Write using git tag message as template", "Leave blank"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Leave blank")
					})
				pm.RegisterSelect("Submit?",
					[]string{"Publish release", "Save as draft", "Cancel"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Publish release")
					})
				pm.RegisterInput("Title (optional)", func(_, d string) (string, error) {
					return d, nil
				})
				pm.RegisterConfirm("Is this a prerelease?", func(_ string, _ bool) (bool, error) {
					return false, nil
				})
			},
			runStubs: func(rs *run.CommandStubber) {
				rs.Register(contentCmd, 0, "tag exists")
				rs.Register(signatureCmd, 0, "")
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.StatusStringResponse(200, `{
						"name": "generated name",
						"body": "generated body"
					}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`))
			},
			wantParams: map[string]interface{}{
				"draft":            false,
				"name":             "generated name",
				"prerelease":       false,
				"tag_name":         "v1.2.3",
				"target_commitish": "main",
			},
			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
		},
		{
			name: "create a release using generated notes with previous tag",
			opts: &CreateOptions{
				TagName:       "v1.2.3",
				NotesStartTag: "v1.1.0",
			},
			prompterStubs: func(t *testing.T, pm *prompter.MockPrompter) {
				pm.RegisterSelect("Release notes",
					[]string{"Write my own", "Write using generated notes as template", "Leave blank"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Write using generated notes as template")
					})
				pm.RegisterSelect("Submit?",
					[]string{"Publish release", "Save as draft", "Cancel"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Publish release")
					})
				pm.RegisterInput("Title (optional)", func(_, d string) (string, error) {
					return d, nil
				})
				pm.RegisterConfirm("Is this a prerelease?", func(_ string, _ bool) (bool, error) {
					return false, nil
				})
			},
			runStubs: defaultRunStubs,
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.RESTPayload(200, `{
						"name": "generated name",
						"body": "generated body"
				}`, func(params map[string]interface{}) {
						assert.Equal(t, map[string]interface{}{
							"tag_name":          "v1.2.3",
							"previous_tag_name": "v1.1.0",
						}, params)
					}))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
					httpmock.StatusStringResponse(201, `{
						"url": "https://api.github.com/releases/123",
						"upload_url": "https://api.github.com/assets/upload",
						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
					}`))
			},
			wantParams: map[string]interface{}{
				"body":       "generated body",
				"draft":      false,
				"name":       "generated name",
				"prerelease": false,
				"tag_name":   "v1.2.3",
			},
			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
		},
		{
			name: "create a release using commit log as notes with previous tag",
			opts: &CreateOptions{
				TagName:       "v1.2.3",
				NotesStartTag: "v1.1.0",
			},
			prompterStubs: func(t *testing.T, pm *prompter.MockPrompter) {
				pm.RegisterSelect("Release notes",
					[]string{"Write my own", "Write using commit log as template", "Leave blank"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Write using commit log as template")
					})
				pm.RegisterSelect("Submit?",
					[]string{"Publish release", "Save as draft", "Cancel"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Publish release")
					})
				pm.RegisterInput("Title (optional)", func(_, d string) (string, error) {
					return d, nil
				})
				pm.RegisterConfirm("Is this a prerelease?", func(_ string, _ bool) (bool, error) {
					return false, nil
				})
			},
			runStubs: func(rs *run.CommandStubber) {
				defaultRunStubs(rs)
				rs.Register(`git .+log .+v1\.1\.0\.\.HEAD$`, 0, "commit subject\n\ncommit body\n")
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.StatusStringResponse(404, `{}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
					httpmock.StatusStringResponse(201, `{
						"url": "https://api.github.com/releases/123",
						"upload_url": "https://api.github.com/assets/upload",
						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
					}`))
			},
			wantParams: map[string]interface{}{
				"body":       "* commit subject\n\n  commit body\n  ",
				"draft":      false,
				"prerelease": false,
				"tag_name":   "v1.2.3",
			},
			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
		},
		{
			name: "create a release when remote tag exists and verify-tag flag is set",
			opts: &CreateOptions{
				TagName:   "v1.2.3",
				VerifyTag: true,
			},
			prompterStubs: func(t *testing.T, pm *prompter.MockPrompter) {
				pm.RegisterSelect("Release notes",
					[]string{"Write my own", "Write using generated notes as template", "Write using git tag message as template", "Leave blank"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Leave blank")
					})
				pm.RegisterSelect("Submit?",
					[]string{"Publish release", "Save as draft", "Cancel"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "Publish release")
					})

				pm.RegisterInput("Title (optional)", func(_, d string) (string, error) {
					return d, nil
				})

				pm.RegisterConfirm("Is this a prerelease?", func(_ string, _ bool) (bool, error) {
					return false, nil
				})
			},
			runStubs: func(rs *run.CommandStubber) {
				rs.Register(contentCmd, 0, "tag exists")
				rs.Register(signatureCmd, 0, "")
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.GraphQL("RepositoryFindRef"),
					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
					httpmock.StatusStringResponse(200, `{
						"name": "generated name",
						"body": "generated body"
					}`))
				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
					"url": "https://api.github.com/releases/123",
					"upload_url": "https://api.github.com/assets/upload",
					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
				}`))
			},
			wantParams: map[string]interface{}{
				"draft":      false,
				"name":       "generated name",
				"prerelease": false,
				"tag_name":   "v1.2.3",
			},
			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
		},
		{
			name: "error when remote tag does not exist and verify-tag flag is set",
			opts: &CreateOptions{
				TagName:   "v1.2.3",
				VerifyTag: true,
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.GraphQL("RepositoryFindRef"),
					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": ""}}}}`))
			},
			wantErr: "tag v1.2.3 doesn't exist in the repo OWNER/REPO, aborting due to --verify-tag flag",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, _, stdout, stderr := iostreams.Test()
			ios.SetStdoutTTY(true)
			ios.SetStdinTTY(true)
			ios.SetStderrTTY(true)
			tt.opts.IO = ios

			reg := &httpmock.Registry{}
			defer reg.Verify(t)
			tt.httpStubs(reg)
			tt.opts.HttpClient = func() (*http.Client, error) {
				return &http.Client{Transport: reg}, nil
			}

			tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
				return ghrepo.FromFullName("OWNER/REPO")
			}

			tt.opts.Config = func() (gh.Config, error) {
				return config.NewBlankConfig(), nil
			}

			tt.opts.Edit = func(_, _, val string, _ io.Reader, _, _ io.Writer) (string, error) {
				return val, nil
			}

			tt.opts.GitClient = &git.Client{GitPath: "some/path/git"}

			pm := prompter.NewMockPrompter(t)
			if tt.prompterStubs != nil {
				tt.prompterStubs(t, pm)
			}
			tt.opts.Prompter = pm

			rs, teardown := run.Stub()
			defer teardown(t)
			if tt.runStubs != nil {
				tt.runStubs(rs)
			}

			err := createRun(tt.opts)

			if tt.wantErr != "" {
				require.EqualError(t, err, tt.wantErr)
				return
			} else {
				require.NoError(t, err)
			}

			if tt.wantParams != nil {
				var r *http.Request
				for _, req := range reg.Requests {
					if req.URL.Path == "/repos/OWNER/REPO/releases" {
						r = req
						break
					}
				}
				if r == nil {
					t.Fatalf("no http requests for creating a release found")
				}
				bb, err := io.ReadAll(r.Body)
				assert.NoError(t, err)
				var params map[string]interface{}
				err = json.Unmarshal(bb, &params)
				assert.NoError(t, err)
				assert.Equal(t, tt.wantParams, params)
			}

			assert.Equal(t, tt.wantOut, stdout.String())
			assert.Equal(t, "", stderr.String())
		})
	}
}

func Test_gitTagInfo(t *testing.T) {
	const tagName = "foo"
	const contentCmd = `git tag --list foo --format=%\(contents\)`
	const signatureCmd = `git tag --list foo --format=%\(contents:signature\)`

	tests := []struct {
		name       string
		runStubs   func(*run.CommandStubber)
		wantErr    string
		wantResult string
	}{
		{
			name: "no signature",
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(contentCmd, 0, "some\nmultiline\ncontent")
				cs.Register(signatureCmd, 0, "")
			},
			wantResult: "some\nmultiline\ncontent",
		},
		{
			name: "with signature (PGP)",
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(contentCmd, 0, "some\nmultiline\ncontent\n-----BEGIN PGP SIGNATURE-----\n\nfoo\n-----END PGP SIGNATURE-----")
				cs.Register(signatureCmd, 0, "-----BEGIN PGP SIGNATURE-----\n\nfoo\n-----END PGP SIGNATURE-----")
			},
			wantResult: "some\nmultiline\ncontent",
		},
		{
			name: "with signature (PGP, RFC1991)",
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(contentCmd, 0, "some\nmultiline\ncontent\n-----BEGIN PGP MESSAGE-----\n\nfoo\n-----END PGP MESSAGE-----")
				cs.Register(signatureCmd, 0, "-----BEGIN PGP MESSAGE-----\n\nfoo\n-----END PGP MESSAGE-----")
			},
			wantResult: "some\nmultiline\ncontent",
		},
		{
			name: "with signature (SSH)",
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(contentCmd, 0, "some\nmultiline\ncontent\n-----BEGIN SSH SIGNATURE-----\nfoo\n-----END SSH SIGNATURE-----")
				cs.Register(signatureCmd, 0, "-----BEGIN SSH SIGNATURE-----\nfoo\n-----END SSH SIGNATURE-----")
			},
			wantResult: "some\nmultiline\ncontent",
		},
		{
			name: "with signature (X.509)",
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(contentCmd, 0, "some\nmultiline\ncontent\n-----BEGIN SIGNED MESSAGE-----\nfoo\n-----END SIGNED MESSAGE-----")
				cs.Register(signatureCmd, 0, "-----BEGIN SIGNED MESSAGE-----\nfoo\n-----END SIGNED MESSAGE-----")
			},
			wantResult: "some\nmultiline\ncontent",
		},
		{
			name: "with signature in content but not as true signature",
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(contentCmd, 0, "some\nmultiline\ncontent\n-----BEGIN PGP SIGNATURE-----\n\nfoo\n-----END PGP SIGNATURE-----")
				cs.Register(signatureCmd, 0, "")
			},
			wantResult: "some\nmultiline\ncontent\n-----BEGIN PGP SIGNATURE-----\n\nfoo\n-----END PGP SIGNATURE-----",
		},
		{
			name: "error getting content",
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(contentCmd, 1, "some error")
			},
			wantErr: fmt.Sprintf("failed to run git: %s exited with status 1", contentCmd),
		},
		{
			name: "error getting signature",
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(contentCmd, 0, "whatever")
				cs.Register(signatureCmd, 1, "some error")
			},
			wantErr: fmt.Sprintf("failed to run git: %s exited with status 1", signatureCmd),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gitClient := &git.Client{GitPath: "some/path/git"}

			rs, teardown := run.Stub()
			defer teardown(t)
			if tt.runStubs != nil {
				tt.runStubs(rs)
			}

			result, err := gitTagInfo(gitClient, tagName)

			if tt.wantErr != "" {
				require.EqualError(t, err, tt.wantErr)
				return
			} else {
				require.NoError(t, err)
			}

			assert.Equal(t, tt.wantResult, result)
		})
	}
}

func boolPtr(b bool) *bool {
	return &b
}
