package create

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/MakeNowJust/heredoc"
	"github.com/cli/cli/v2/api"
	"github.com/cli/cli/v2/context"
	"github.com/cli/cli/v2/git"
	"github.com/cli/cli/v2/internal/browser"
	"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/pr/shared"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/httpmock"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/cli/cli/v2/test"
	"github.com/google/shlex"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewCmdCreate(t *testing.T) {
	tmpFile := filepath.Join(t.TempDir(), "my-body.md")
	err := os.WriteFile(tmpFile, []byte("a body from file"), 0600)
	require.NoError(t, err)

	tests := []struct {
		name      string
		tty       bool
		stdin     string
		cli       string
		config    string
		wantsErr  bool
		wantsOpts CreateOptions
	}{
		{
			name:     "empty non-tty",
			tty:      false,
			cli:      "",
			wantsErr: true,
		},
		{
			name:     "only title non-tty",
			tty:      false,
			cli:      "--title mytitle",
			wantsErr: true,
		},
		{
			name:     "minimum non-tty",
			tty:      false,
			cli:      "--title mytitle --body ''",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:               "mytitle",
				TitleProvided:       true,
				Body:                "",
				BodyProvided:        true,
				Autofill:            false,
				RecoverFile:         "",
				WebMode:             false,
				IsDraft:             false,
				BaseBranch:          "",
				HeadBranch:          "",
				MaintainerCanModify: true,
			},
		},
		{
			name:     "empty tty",
			tty:      true,
			cli:      "",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:               "",
				TitleProvided:       false,
				Body:                "",
				BodyProvided:        false,
				Autofill:            false,
				RecoverFile:         "",
				WebMode:             false,
				IsDraft:             false,
				BaseBranch:          "",
				HeadBranch:          "",
				MaintainerCanModify: true,
			},
		},
		{
			name:     "body from stdin",
			tty:      false,
			stdin:    "this is on standard input",
			cli:      "-t mytitle -F -",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:               "mytitle",
				TitleProvided:       true,
				Body:                "this is on standard input",
				BodyProvided:        true,
				Autofill:            false,
				RecoverFile:         "",
				WebMode:             false,
				IsDraft:             false,
				BaseBranch:          "",
				HeadBranch:          "",
				MaintainerCanModify: true,
			},
		},
		{
			name:     "body from file",
			tty:      false,
			cli:      fmt.Sprintf("-t mytitle -F '%s'", tmpFile),
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:               "mytitle",
				TitleProvided:       true,
				Body:                "a body from file",
				BodyProvided:        true,
				Autofill:            false,
				RecoverFile:         "",
				WebMode:             false,
				IsDraft:             false,
				BaseBranch:          "",
				HeadBranch:          "",
				MaintainerCanModify: true,
			},
		},
		{
			name:     "template from file name tty",
			tty:      true,
			cli:      "-t mytitle --template bug_fix.md",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:               "mytitle",
				TitleProvided:       true,
				Body:                "",
				BodyProvided:        false,
				Autofill:            false,
				RecoverFile:         "",
				WebMode:             false,
				IsDraft:             false,
				BaseBranch:          "",
				HeadBranch:          "",
				MaintainerCanModify: true,
				Template:            "bug_fix.md",
			},
		},
		{
			name:     "template from file name non-tty",
			tty:      false,
			cli:      "-t mytitle --template bug_fix.md",
			wantsErr: true,
		},
		{
			name:     "template and body",
			tty:      false,
			cli:      `-t mytitle --template bug_fix.md --body "pr body"`,
			wantsErr: true,
		},
		{
			name:     "template and body file",
			tty:      false,
			cli:      "-t mytitle --template bug_fix.md --body-file body_file.md",
			wantsErr: true,
		},
		{
			name:     "fill-first",
			tty:      false,
			cli:      "--fill-first",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:               "",
				TitleProvided:       false,
				Body:                "",
				BodyProvided:        false,
				Autofill:            false,
				FillFirst:           true,
				RecoverFile:         "",
				WebMode:             false,
				IsDraft:             false,
				BaseBranch:          "",
				HeadBranch:          "",
				MaintainerCanModify: true,
			},
		},
		{
			name:     "fill and fill-first",
			tty:      false,
			cli:      "--fill --fill-first",
			wantsErr: true,
		},
		{
			name:     "dry-run and web",
			tty:      false,
			cli:      "--web --dry-run",
			wantsErr: true,
		},
		{
			name:     "editor by cli",
			tty:      true,
			cli:      "--editor",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:               "",
				Body:                "",
				RecoverFile:         "",
				WebMode:             false,
				EditorMode:          true,
				MaintainerCanModify: true,
			},
		},
		{
			name:     "editor by config",
			tty:      true,
			cli:      "",
			config:   "prefer_editor_prompt: enabled",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:               "",
				Body:                "",
				RecoverFile:         "",
				WebMode:             false,
				EditorMode:          true,
				MaintainerCanModify: true,
			},
		},
		{
			name:     "editor and web",
			tty:      true,
			cli:      "--editor --web",
			wantsErr: true,
		},
		{
			name:     "can use web even though editor is enabled by config",
			tty:      true,
			cli:      `--web --title mytitle --body "issue body"`,
			config:   "prefer_editor_prompt: enabled",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:               "mytitle",
				Body:                "issue body",
				TitleProvided:       true,
				BodyProvided:        true,
				RecoverFile:         "",
				WebMode:             true,
				EditorMode:          false,
				MaintainerCanModify: true,
			},
		},
		{
			name:     "editor with non-tty",
			tty:      false,
			cli:      "--editor",
			wantsErr: true,
		},
		{
			name: "fill and base",
			cli:  "--fill --base trunk",
			wantsOpts: CreateOptions{
				Autofill:            true,
				BaseBranch:          "trunk",
				MaintainerCanModify: true,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, stdin, stdout, stderr := iostreams.Test()
			if tt.stdin != "" {
				_, _ = stdin.WriteString(tt.stdin)
			} else if tt.tty {
				ios.SetStdinTTY(true)
				ios.SetStdoutTTY(true)
			}

			f := &cmdutil.Factory{
				IOStreams: ios,
				Config: func() (gh.Config, error) {
					if tt.config != "" {
						return config.NewFromString(tt.config), nil
					}
					return config.NewBlankConfig(), nil
				},
			}

			var opts *CreateOptions
			cmd := NewCmdCreate(f, func(o *CreateOptions) error {
				opts = o
				return nil
			})

			args, err := shlex.Split(tt.cli)
			require.NoError(t, err)
			cmd.SetArgs(args)
			cmd.SetOut(stderr)
			cmd.SetErr(stderr)
			_, err = cmd.ExecuteC()
			if tt.wantsErr {
				assert.Error(t, err)
				return
			} else {
				require.NoError(t, err)
			}

			assert.Equal(t, "", stdout.String())
			assert.Equal(t, "", stderr.String())

			assert.Equal(t, tt.wantsOpts.Body, opts.Body)
			assert.Equal(t, tt.wantsOpts.BodyProvided, opts.BodyProvided)
			assert.Equal(t, tt.wantsOpts.Title, opts.Title)
			assert.Equal(t, tt.wantsOpts.TitleProvided, opts.TitleProvided)
			assert.Equal(t, tt.wantsOpts.Autofill, opts.Autofill)
			assert.Equal(t, tt.wantsOpts.FillFirst, opts.FillFirst)
			assert.Equal(t, tt.wantsOpts.WebMode, opts.WebMode)
			assert.Equal(t, tt.wantsOpts.RecoverFile, opts.RecoverFile)
			assert.Equal(t, tt.wantsOpts.IsDraft, opts.IsDraft)
			assert.Equal(t, tt.wantsOpts.MaintainerCanModify, opts.MaintainerCanModify)
			assert.Equal(t, tt.wantsOpts.BaseBranch, opts.BaseBranch)
			assert.Equal(t, tt.wantsOpts.HeadBranch, opts.HeadBranch)
			assert.Equal(t, tt.wantsOpts.Template, opts.Template)
		})
	}
}

func Test_createRun(t *testing.T) {
	tests := []struct {
		name               string
		setup              func(*CreateOptions, *testing.T) func()
		cmdStubs           func(*run.CommandStubber)
		promptStubs        func(*prompter.PrompterMock)
		httpStubs          func(*httpmock.Registry, *testing.T)
		expectedOutputs    []string
		expectedOut        string
		expectedErrOut     string
		expectedBrowse     string
		wantErr            string
		tty                bool
		customBranchConfig bool
	}{
		{
			name: "nontty web",
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.WebMode = true
				opts.HeadBranch = "feature"
				return func() {}
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
			},
			expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1",
		},
		{
			name: "nontty",
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
						{ "data": { "createPullRequest": { "pullRequest": {
							"URL": "https://github.com/OWNER/REPO/pull/12"
						} } } }`,
						func(input map[string]interface{}) {
							assert.Equal(t, "REPOID", input["repositoryId"])
							assert.Equal(t, "my title", input["title"])
							assert.Equal(t, "my body", input["body"])
							assert.Equal(t, "master", input["baseRefName"])
							assert.Equal(t, "feature", input["headRefName"])
						}))
			},
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "my title"
				opts.Body = "my body"
				opts.HeadBranch = "feature"
				return func() {}
			},
			expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
		},
		{
			name: "dry-run-nontty-with-default-base",
			tty:  false,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "my title"
				opts.Body = "my body"
				opts.HeadBranch = "feature"
				opts.DryRun = true
				return func() {}
			},
			expectedOutputs: []string{
				"Would have created a Pull Request with:",
				`title:	my title`,
				`draft:	false`,
				`base:	master`,
				`head:	feature`,
				`maintainerCanModify:	false`,
				`body:`,
				`my body`,
				``,
			},
			expectedErrOut: "",
		},
		{
			name: "dry-run-nontty-with-all-opts",
			tty:  false,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "TITLE"
				opts.Body = "BODY"
				opts.BaseBranch = "trunk"
				opts.HeadBranch = "feature"
				opts.Assignees = []string{"monalisa"}
				opts.Labels = []string{"bug", "todo"}
				opts.Projects = []string{"roadmap"}
				opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"}
				opts.Milestone = "big one.oh"
				opts.DryRun = true
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
					httpmock.StringResponse(`
					{ "data": {
						"u000": { "login": "MonaLisa", "id": "MONAID" },
						"u001": { "login": "hubot", "id": "HUBOTID" },
						"repository": {
							"l000": { "name": "bug", "id": "BUGID" },
							"l001": { "name": "TODO", "id": "TODOID" }
						},
						"organization": {
							"t000": { "slug": "core", "id": "COREID" },
							"t001": { "slug": "robots", "id": "ROBOTID" }
						}
					} }
					`))
				reg.Register(
					httpmock.GraphQL(`query RepositoryMilestoneList\b`),
					httpmock.StringResponse(`
					{ "data": { "repository": { "milestones": {
						"nodes": [
							{ "title": "GA", "id": "GAID" },
							{ "title": "Big One.oh", "id": "BIGONEID" }
						],
						"pageInfo": { "hasNextPage": false }
					} } } }
					`))
				mockRetrieveProjects(t, reg)
			},
			expectedOutputs: []string{
				"Would have created a Pull Request with:",
				`title:	TITLE`,
				`draft:	false`,
				`base:	trunk`,
				`head:	feature`,
				`labels:	bug, todo`,
				`reviewers:	hubot, monalisa, /core, /robots`,
				`assignees:	monalisa`,
				`milestones:	big one.oh`,
				`projects:	roadmap`,
				`maintainerCanModify:	false`,
				`body:`,
				`BODY`,
				``,
			},
			expectedErrOut: "",
		},
		{
			name: "dry-run-tty-with-default-base",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "my title"
				opts.Body = "my body"
				opts.HeadBranch = "feature"
				opts.DryRun = true
				return func() {}
			},
			expectedOutputs: []string{
				`Would have created a Pull Request with:`,
				`Title: my title`,
				`Draft: false`,
				`Base: master`,
				`Head: feature`,
				`MaintainerCanModify: false`,
				`Body:`,
				``,
				`  my body                                                                         `,
				``,
				``,
			},
			expectedErrOut: heredoc.Doc(`

			Dry Running pull request for feature into master in OWNER/REPO

		`),
		},
		{
			name: "dry-run-tty-with-all-opts",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "TITLE"
				opts.Body = "BODY"
				opts.BaseBranch = "trunk"
				opts.HeadBranch = "feature"
				opts.Assignees = []string{"monalisa"}
				opts.Labels = []string{"bug", "todo"}
				opts.Projects = []string{"roadmap"}
				opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"}
				opts.Milestone = "big one.oh"
				opts.DryRun = true
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
					httpmock.StringResponse(`
					{ "data": {
						"u000": { "login": "MonaLisa", "id": "MONAID" },
						"u001": { "login": "hubot", "id": "HUBOTID" },
						"repository": {
							"l000": { "name": "bug", "id": "BUGID" },
							"l001": { "name": "TODO", "id": "TODOID" }
						},
						"organization": {
							"t000": { "slug": "core", "id": "COREID" },
							"t001": { "slug": "robots", "id": "ROBOTID" }
						}
					} }
					`))
				reg.Register(
					httpmock.GraphQL(`query RepositoryMilestoneList\b`),
					httpmock.StringResponse(`
					{ "data": { "repository": { "milestones": {
						"nodes": [
							{ "title": "GA", "id": "GAID" },
							{ "title": "Big One.oh", "id": "BIGONEID" }
						],
						"pageInfo": { "hasNextPage": false }
					} } } }
					`))
				mockRetrieveProjects(t, reg)
			},
			expectedOutputs: []string{
				`Would have created a Pull Request with:`,
				`Title: TITLE`,
				`Draft: false`,
				`Base: trunk`,
				`Head: feature`,
				`Labels: bug, todo`,
				`Reviewers: hubot, monalisa, /core, /robots`,
				`Assignees: monalisa`,
				`Milestones: big one.oh`,
				`Projects: roadmap`,
				`MaintainerCanModify: false`,
				`Body:`,
				``,
				`  BODY                                                                            `,
				``,
				``,
			},
			expectedErrOut: heredoc.Doc(`

			Dry Running pull request for feature into trunk in OWNER/REPO

		`),
		},
		{
			name: "dry-run-tty-with-empty-body",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "TITLE"
				opts.Body = ""
				opts.HeadBranch = "feature"
				opts.DryRun = true
				return func() {}
			},
			expectedOut: heredoc.Doc(`
				Would have created a Pull Request with:
				Title: TITLE
				Draft: false
				Base: master
				Head: feature
				MaintainerCanModify: false
				Body:
				No description provided
			`),
			expectedErrOut: heredoc.Doc(`

			Dry Running pull request for feature into master in OWNER/REPO

		`),
		},
		{
			name: "survey",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "my title"
				opts.Body = "my body"
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.StubRepoResponse("OWNER", "REPO")
				reg.Register(
					httpmock.GraphQL(`query UserCurrent\b`),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
							{ "data": { "createPullRequest": { "pullRequest": {
								"URL": "https://github.com/OWNER/REPO/pull/12"
							} } } }`, func(input map[string]interface{}) {
						assert.Equal(t, "REPOID", input["repositoryId"].(string))
						assert.Equal(t, "my title", input["title"].(string))
						assert.Equal(t, "my body", input["body"].(string))
						assert.Equal(t, "master", input["baseRefName"].(string))
						assert.Equal(t, "feature", input["headRefName"].(string))
						assert.Equal(t, false, input["draft"].(bool))
					}))
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
				cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					if p == "Where should we push the 'feature' branch?" {
						return 0, nil
					} else {
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
		},
		{
			name: "project v2",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "my title"
				opts.Body = "my body"
				opts.Projects = []string{"RoadmapV2"}
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.StubRepoResponse("OWNER", "REPO")
				reg.Register(
					httpmock.GraphQL(`query UserCurrent\b`),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
				mockRetrieveProjects(t, reg)
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
							{ "data": { "createPullRequest": { "pullRequest": {
								"id": "PullRequest#1",
								"URL": "https://github.com/OWNER/REPO/pull/12"
							} } } }
							`, func(input map[string]interface{}) {
						assert.Equal(t, "REPOID", input["repositoryId"].(string))
						assert.Equal(t, "my title", input["title"].(string))
						assert.Equal(t, "my body", input["body"].(string))
						assert.Equal(t, "master", input["baseRefName"].(string))
						assert.Equal(t, "feature", input["headRefName"].(string))
						assert.Equal(t, false, input["draft"].(bool))
					}))
				reg.Register(
					httpmock.GraphQL(`mutation UpdateProjectV2Items\b`),
					httpmock.GraphQLQuery(`
							{ "data": { "add_000": { "item": {
								"id": "1"
							} } } }
							`, func(mutations string, inputs map[string]interface{}) {
						variables, err := json.Marshal(inputs)
						assert.NoError(t, err)
						expectedMutations := "mutation UpdateProjectV2Items($input_000: AddProjectV2ItemByIdInput!) {add_000: addProjectV2ItemById(input: $input_000) { item { id } }}"
						expectedVariables := `{"input_000":{"contentId":"PullRequest#1","projectId":"ROADMAPV2ID"}}`
						assert.Equal(t, expectedMutations, mutations)
						assert.Equal(t, expectedVariables, string(variables))
					}))
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
				cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					if p == "Where should we push the 'feature' branch?" {
						return 0, nil
					} else {
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
		},
		{
			name: "no maintainer modify",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "my title"
				opts.Body = "my body"
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.StubRepoResponse("OWNER", "REPO")
				reg.Register(
					httpmock.GraphQL(`query UserCurrent\b`),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
							{ "data": { "createPullRequest": { "pullRequest": {
								"URL": "https://github.com/OWNER/REPO/pull/12"
							} } } }
							`, func(input map[string]interface{}) {
						assert.Equal(t, false, input["maintainerCanModify"].(bool))
						assert.Equal(t, "REPOID", input["repositoryId"].(string))
						assert.Equal(t, "my title", input["title"].(string))
						assert.Equal(t, "my body", input["body"].(string))
						assert.Equal(t, "master", input["baseRefName"].(string))
						assert.Equal(t, "feature", input["headRefName"].(string))
					}))
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
				cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					if p == "Where should we push the 'feature' branch?" {
						return 0, nil
					} else {
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
		},
		{
			name: "create fork",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "title"
				opts.Body = "body"
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.StubRepoResponse("OWNER", "REPO")
				reg.Register(
					httpmock.GraphQL(`query UserCurrent\b`),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`))
				reg.Register(
					httpmock.REST("POST", "repos/OWNER/REPO/forks"),
					httpmock.StatusStringResponse(201, `
							{ "node_id": "NODEID",
							  "name": "REPO",
							  "owner": {"login": "monalisa"}
							}`))
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
							{ "data": { "createPullRequest": { "pullRequest": {
								"URL": "https://github.com/OWNER/REPO/pull/12"
							}}}}`, func(input map[string]interface{}) {
						assert.Equal(t, "REPOID", input["repositoryId"].(string))
						assert.Equal(t, "master", input["baseRefName"].(string))
						assert.Equal(t, "monalisa:feature", input["headRefName"].(string))
					}))
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
				cs.Register("git remote rename origin upstream", 0, "")
				cs.Register(`git remote add origin https://github.com/monalisa/REPO.git`, 0, "")
				cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
				cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "")
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					if p == "Where should we push the 'feature' branch?" {
						return prompter.IndexFor(opts, "Create a fork of OWNER/REPO")
					} else {
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\nChanged OWNER/REPO remote to \"upstream\"\nAdded monalisa/REPO as remote \"origin\"\n! Repository monalisa/REPO set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n",
		},
		{
			name: "pushed to non base repo",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "title"
				opts.Body = "body"
				opts.Remotes = func() (context.Remotes, error) {
					return context.Remotes{
						{
							Remote: &git.Remote{
								Name:     "upstream",
								Resolved: "base",
							},
							Repo: ghrepo.New("OWNER", "REPO"),
						},
						{
							Remote: &git.Remote{
								Name:     "origin",
								Resolved: "base",
							},
							Repo: ghrepo.New("monalisa", "REPO"),
						},
					}, nil
				}
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
							{ "data": { "createPullRequest": { "pullRequest": {
								"URL": "https://github.com/OWNER/REPO/pull/12"
							} } } }`, func(input map[string]interface{}) {
						assert.Equal(t, "REPOID", input["repositoryId"].(string))
						assert.Equal(t, "master", input["baseRefName"].(string))
						assert.Equal(t, "monalisa:feature", input["headRefName"].(string))
					}))
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register("git show-ref --verify", 0, heredoc.Doc(`
			deadbeef HEAD
			deadb00f refs/remotes/upstream/feature
			deadbeef refs/remotes/origin/feature`)) // determineTrackingBranch
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\n",
		},
		{
			name: "pushed to different branch name",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "title"
				opts.Body = "body"
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
			{ "data": { "createPullRequest": { "pullRequest": {
				"URL": "https://github.com/OWNER/REPO/pull/12"
			} } } }
			`, func(input map[string]interface{}) {
						assert.Equal(t, "REPOID", input["repositoryId"].(string))
						assert.Equal(t, "master", input["baseRefName"].(string))
						assert.Equal(t, "my-feat2", input["headRefName"].(string))
					}))
			},
			customBranchConfig: true,
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git config --get-regexp \^branch\\\.feature\\\.`, 0, heredoc.Doc(`
			branch.feature.remote origin
			branch.feature.merge refs/heads/my-feat2
		`)) // determineTrackingBranch
				cs.Register("git show-ref --verify", 0, heredoc.Doc(`
			deadbeef HEAD
			deadbeef refs/remotes/origin/my-feat2
		`)) // determineTrackingBranch
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for my-feat2 into master in OWNER/REPO\n\n",
		},
		{
			name: "non legacy template",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.Title = "my title"
				opts.HeadBranch = "feature"
				opts.RootDirOverride = "./fixtures/repoWithNonLegacyPRTemplates"
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query PullRequestTemplates\b`),
					httpmock.StringResponse(`
					{ "data": { "repository": { "pullRequestTemplates": [
						{ "filename": "template1",
						  "body": "this is a bug" },
						{ "filename": "template2",
						  "body": "this is a enhancement" }
					] } } }`))
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
				{ "data": { "createPullRequest": { "pullRequest": {
					"URL": "https://github.com/OWNER/REPO/pull/12"
				} } } }
				`, func(input map[string]interface{}) {
						assert.Equal(t, "my title", input["title"].(string))
						assert.Equal(t, "- **commit 1**\n- **commit 0**\n\nthis is a bug", input["body"].(string))
					}))
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "d3476a1\u0000commit 0\u0000\u0000\n7a6ea13\u0000commit 1\u0000\u0000")
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.MarkdownEditorFunc = func(p, d string, ba bool) (string, error) {
					if p == "Body" {
						return d, nil
					} else {
						return "", prompter.NoSuchPromptErr(p)
					}
				}
				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					switch p {
					case "What's next?":
						return 0, nil
					case "Choose a template":
						return prompter.IndexFor(opts, "template1")
					default:
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
		},
		{
			name: "metadata",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.Title = "TITLE"
				opts.BodyProvided = true
				opts.Body = "BODY"
				opts.HeadBranch = "feature"
				opts.Assignees = []string{"monalisa"}
				opts.Labels = []string{"bug", "todo"}
				opts.Projects = []string{"roadmap"}
				opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"}
				opts.Milestone = "big one.oh"
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
					httpmock.StringResponse(`
					{ "data": {
						"u000": { "login": "MonaLisa", "id": "MONAID" },
						"u001": { "login": "hubot", "id": "HUBOTID" },
						"repository": {
							"l000": { "name": "bug", "id": "BUGID" },
							"l001": { "name": "TODO", "id": "TODOID" }
						},
						"organization": {
							"t000": { "slug": "core", "id": "COREID" },
							"t001": { "slug": "robots", "id": "ROBOTID" }
						}
					} }
					`))
				reg.Register(
					httpmock.GraphQL(`query RepositoryMilestoneList\b`),
					httpmock.StringResponse(`
					{ "data": { "repository": { "milestones": {
						"nodes": [
							{ "title": "GA", "id": "GAID" },
							{ "title": "Big One.oh", "id": "BIGONEID" }
						],
						"pageInfo": { "hasNextPage": false }
					} } } }
					`))
				mockRetrieveProjects(t, reg)
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
					{ "data": { "createPullRequest": { "pullRequest": {
						"id": "NEWPULLID",
						"URL": "https://github.com/OWNER/REPO/pull/12"
					} } } }
				`, func(inputs map[string]interface{}) {
						assert.Equal(t, "TITLE", inputs["title"])
						assert.Equal(t, "BODY", inputs["body"])
						if v, ok := inputs["assigneeIds"]; ok {
							t.Errorf("did not expect assigneeIds: %v", v)
						}
						if v, ok := inputs["userIds"]; ok {
							t.Errorf("did not expect userIds: %v", v)
						}
					}))
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreateMetadata\b`),
					httpmock.GraphQLMutation(`
					{ "data": { "updatePullRequest": {
						"clientMutationId": ""
					} } }
				`, func(inputs map[string]interface{}) {
						assert.Equal(t, "NEWPULLID", inputs["pullRequestId"])
						assert.Equal(t, []interface{}{"MONAID"}, inputs["assigneeIds"])
						assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
						assert.Equal(t, []interface{}{"ROADMAPID"}, inputs["projectIds"])
						assert.Equal(t, "BIGONEID", inputs["milestoneId"])
					}))
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`),
					httpmock.GraphQLMutation(`
					{ "data": { "requestReviews": {
						"clientMutationId": ""
					} } }
				`, func(inputs map[string]interface{}) {
						assert.Equal(t, "NEWPULLID", inputs["pullRequestId"])
						assert.Equal(t, []interface{}{"HUBOTID", "MONAID"}, inputs["userIds"])
						assert.Equal(t, []interface{}{"COREID", "ROBOTID"}, inputs["teamIds"])
						assert.Equal(t, true, inputs["union"])
					}))
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
		},
		{
			name: "already exists",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "title"
				opts.Body = "body"
				opts.HeadBranch = "feature"
				opts.Finder = shared.NewMockFinder("feature", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO"))
				return func() {}
			},
			wantErr: "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123",
		},
		{
			name: "web",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.WebMode = true
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.StubRepoResponse("OWNER", "REPO")
				reg.Register(
					httpmock.GraphQL(`query UserCurrent\b`),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
				cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
				cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					if p == "Where should we push the 'feature' branch?" {
						return 0, nil
					} else {
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			expectedErrOut: "Opening https://github.com/OWNER/REPO/compare/master...feature in your browser.\n",
			expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1",
		},
		{
			name: "web project",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.WebMode = true
				opts.Projects = []string{"Triage"}
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.StubRepoResponse("OWNER", "REPO")
				reg.Register(
					httpmock.GraphQL(`query UserCurrent\b`),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
				mockRetrieveProjects(t, reg)
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
				cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
				cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					if p == "Where should we push the 'feature' branch?" {
						return 0, nil
					} else {
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			expectedErrOut: "Opening https://github.com/OWNER/REPO/compare/master...feature in your browser.\n",
			expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1&projects=ORG%2F1",
		},
		{
			name: "draft",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.Title = "my title"
				opts.HeadBranch = "feature"
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query PullRequestTemplates\b`),
					httpmock.StringResponse(`
				{ "data": { "repository": { "pullRequestTemplates": [
					{ "filename": "template1",
					  "body": "this is a bug" },
					{ "filename": "template2",
					  "body": "this is a enhancement" }
				] } } }`),
				)
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
			{ "data": { "createPullRequest": { "pullRequest": {
				"URL": "https://github.com/OWNER/REPO/pull/12"
			} } } }
			`, func(input map[string]interface{}) {
						assert.Equal(t, true, input["draft"].(bool))
					}))
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature`, 0, "")
				cs.Register(`git rev-parse --show-toplevel`, 0, "")
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.MarkdownEditorFunc = func(p, d string, ba bool) (string, error) {
					if p == "Body" {
						return d, nil
					} else {
						return "", prompter.NoSuchPromptErr(p)
					}
				}
				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					switch p {
					case "What's next?":
						return prompter.IndexFor(opts, "Submit as draft")
					case "Choose a template":
						return 0, nil
					default:
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
		},
		{
			name: "recover",
			tty:  true,
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
					httpmock.StringResponse(`
			{ "data": {
				"u000": { "login": "jillValentine", "id": "JILLID" },
				"repository": {},
				"organization": {}
			} }
			`))
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`),
					httpmock.GraphQLMutation(`
			{ "data": { "requestReviews": {
				"clientMutationId": ""
			} } }
		`, func(inputs map[string]interface{}) {
						assert.Equal(t, []interface{}{"JILLID"}, inputs["userIds"])
					}))
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
			{ "data": { "createPullRequest": { "pullRequest": {
				"URL": "https://github.com/OWNER/REPO/pull/12"
			} } } }
			`, func(input map[string]interface{}) {
						assert.Equal(t, "recovered title", input["title"].(string))
						assert.Equal(t, "recovered body", input["body"].(string))
					}))
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.InputFunc = func(p, d string) (string, error) {
					if p == "Title (required)" {
						return d, nil
					} else {
						return "", prompter.NoSuchPromptErr(p)
					}
				}
				pm.MarkdownEditorFunc = func(p, d string, ba bool) (string, error) {
					if p == "Body" {
						return d, nil
					} else {
						return "", prompter.NoSuchPromptErr(p)
					}
				}
				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					if p == "What's next?" {
						return 0, nil
					} else {
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			setup: func(opts *CreateOptions, t *testing.T) func() {
				tmpfile, err := os.CreateTemp(t.TempDir(), "testrecover*")
				assert.NoError(t, err)
				state := shared.IssueMetadataState{
					Title:     "recovered title",
					Body:      "recovered body",
					Reviewers: []string{"jillValentine"},
				}
				data, err := json.Marshal(state)
				assert.NoError(t, err)
				_, err = tmpfile.Write(data)
				assert.NoError(t, err)

				opts.RecoverFile = tmpfile.Name()
				opts.HeadBranch = "feature"
				return func() { tmpfile.Close() }
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
		},
		{
			name: "web long URL",
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
			},
			setup: func(opts *CreateOptions, t *testing.T) func() {
				longBody := make([]byte, 9216)
				opts.Body = string(longBody)
				opts.BodyProvided = true
				opts.WebMode = true
				opts.HeadBranch = "feature"
				return func() {}
			},
			wantErr: "cannot open in browser: maximum URL length exceeded",
		},
		{
			name: "no local git repo",
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.Title = "My PR"
				opts.TitleProvided = true
				opts.Body = ""
				opts.BodyProvided = true
				opts.HeadBranch = "feature"
				opts.RepoOverride = "OWNER/REPO"
				opts.Remotes = func() (context.Remotes, error) {
					return nil, errors.New("not a git repository")
				}
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.StringResponse(`
							{ "data": { "createPullRequest": { "pullRequest": {
								"URL": "https://github.com/OWNER/REPO/pull/12"
							} } } }
						`))
			},
			expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
		},
		{
			name: "single commit title and body are used",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.HeadBranch = "feature"
				return func() {}
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(
					"git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature",
					0,
					"3a9b48085046d156c5acce8f3b3a0532cd706a4a\u0000first commit of pr\u0000first commit description\u0000\n",
				)
				cs.Register(`git rev-parse --show-toplevel`, 0, "")
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					if p == "Where should we push the 'feature' branch?" {
						return 0, nil
					} else {
						return -1, prompter.NoSuchPromptErr(p)
					}
				}

				pm.InputFunc = func(p, d string) (string, error) {
					if p == "Title (required)" {
						return d, nil
					} else if p == "Body" {
						return d, nil
					} else {
						return "", prompter.NoSuchPromptErr(p)
					}
				}

				pm.MarkdownEditorFunc = func(p, d string, ba bool) (string, error) {
					if p == "Body" {
						return d, nil
					} else {
						return "", prompter.NoSuchPromptErr(p)
					}
				}

				pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
					if p == "What's next?" {
						return 0, nil
					} else {
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query PullRequestTemplates\b`),
					httpmock.StringResponse(`{ "data": { "repository": { "pullRequestTemplates": [] } } }`),
				)
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
						{
						"data": { "createPullRequest": { "pullRequest": {
							"URL": "https://github.com/OWNER/REPO/pull/12"
							} } }
						}
						`,
						func(input map[string]interface{}) {
							assert.Equal(t, "first commit of pr", input["title"], "pr title should be first commit message")
							assert.Equal(t, "first commit description", input["body"], "pr body should be first commit description")
						},
					),
				)
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
		},
		{
			name: "fill-first flag provided",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.FillFirst = true
				opts.HeadBranch = "feature"
				return func() {}
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(
					"git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature",
					0,
					"56b6f8bb7c9e3a30093cd17e48934ce354148e80\u0000second commit of pr\u0000\u0000\n"+
						"3a9b48085046d156c5acce8f3b3a0532cd706a4a\u0000first commit of pr\u0000first commit description\u0000\n",
				)
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
						{
						"data": { "createPullRequest": { "pullRequest": {
							"URL": "https://github.com/OWNER/REPO/pull/12"
							} } }
						}
						`,
						func(input map[string]interface{}) {
							assert.Equal(t, "first commit of pr", input["title"], "pr title should be first commit message")
							assert.Equal(t, "first commit description", input["body"], "pr body should be first commit description")
						},
					),
				)
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
		},
		{
			name: "fillverbose flag provided",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.FillVerbose = true
				opts.HeadBranch = "feature"
				return func() {}
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(
					"git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature",
					0,
					"56b6f8bb7c9e3a30093cd17e48934ce354148e80\u0000second commit of pr\u0000second commit description\u0000\n"+
						"3a9b48085046d156c5acce8f3b3a0532cd706a4a\u0000first commit of pr\u0000first commit with super long description, with super long description, with super long description, with super long description.\u0000\n",
				)
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
						{
						"data": { "createPullRequest": { "pullRequest": {
							"URL": "https://github.com/OWNER/REPO/pull/12"
							} } }
						}
						`,
						func(input map[string]interface{}) {
							assert.Equal(t, "feature", input["title"], "pr title should be branch name")
							assert.Equal(t, "- **first commit of pr**\n  first commit with super long description, with super long description, with super long description, with super long description.\n\n- **second commit of pr**\n  second commit description", input["body"], "pr body should be commits msg+body")
						},
					),
				)
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
		},
		{
			name: "editor",
			httpStubs: func(r *httpmock.Registry, t *testing.T) {
				r.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
						{
						"data": { "createPullRequest": { "pullRequest": {
							"URL": "https://github.com/OWNER/REPO/pull/12"
							} } }
						}
				`, func(inputs map[string]interface{}) {
						assert.Equal(t, "title", inputs["title"])
						assert.Equal(t, "body", inputs["body"])
					}))
			},
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.EditorMode = true
				opts.HeadBranch = "feature"
				opts.TitledEditSurvey = func(string, string) (string, string, error) { return "title", "body", nil }
				return func() {}
			},
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register("git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, "")
			},
			expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
		},
		{
			name: "gh-merge-base",
			tty:  true,
			setup: func(opts *CreateOptions, t *testing.T) func() {
				opts.TitleProvided = true
				opts.BodyProvided = true
				opts.Title = "my title"
				opts.Body = "my body"
				opts.Branch = func() (string, error) {
					return "task1", nil
				}
				opts.Remotes = func() (context.Remotes, error) {
					return context.Remotes{
						{
							Remote: &git.Remote{
								Name:     "upstream",
								Resolved: "base",
							},
							Repo: ghrepo.New("OWNER", "REPO"),
						},
						{
							Remote: &git.Remote{
								Name: "origin",
							},
							Repo: ghrepo.New("monalisa", "REPO"),
						},
					}, nil
				}
				return func() {}
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`mutation PullRequestCreate\b`),
					httpmock.GraphQLMutation(`
							{ "data": { "createPullRequest": { "pullRequest": {
								"URL": "https://github.com/OWNER/REPO/pull/12"
							} } } }
							`, func(input map[string]interface{}) {
						assert.Equal(t, "REPOID", input["repositoryId"].(string))
						assert.Equal(t, "my title", input["title"].(string))
						assert.Equal(t, "my body", input["body"].(string))
						assert.Equal(t, "feature/feat2", input["baseRefName"].(string))
						assert.Equal(t, "monalisa:task1", input["headRefName"].(string))
					}))
			},
			customBranchConfig: true,
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git config --get-regexp \^branch\\\.task1\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 0, heredoc.Doc(`
					branch.task1.remote origin
					branch.task1.merge refs/heads/task1
					branch.task1.gh-merge-base feature/feat2`)) // ReadBranchConfig
				cs.Register(`git show-ref --verify`, 0, heredoc.Doc(`
					deadbeef HEAD
					deadb00f refs/remotes/upstream/feature/feat2
					deadbeef refs/remotes/origin/task1`)) // determineTrackingBranch
			},
			expectedOut:    "https://github.com/OWNER/REPO/pull/12\n",
			expectedErrOut: "\nCreating pull request for monalisa:task1 into feature/feat2 in OWNER/REPO\n\n",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			branch := "feature"

			reg := &httpmock.Registry{}
			reg.StubRepoInfoResponse("OWNER", "REPO", "master")
			defer reg.Verify(t)
			if tt.httpStubs != nil {
				tt.httpStubs(reg, t)
			}

			pm := &prompter.PrompterMock{}

			if tt.promptStubs != nil {
				tt.promptStubs(pm)
			}

			cs, cmdTeardown := run.Stub()
			defer cmdTeardown(t)
			cs.Register(`git status --porcelain`, 0, "")
			if !tt.customBranchConfig {
				cs.Register(`git config --get-regexp \^branch\\\..+\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 0, "")
			}

			if tt.cmdStubs != nil {
				tt.cmdStubs(cs)
			}

			opts := CreateOptions{}
			opts.Prompter = pm

			ios, _, stdout, stderr := iostreams.Test()
			// TODO do i need to bother with this
			ios.SetStdoutTTY(tt.tty)
			ios.SetStdinTTY(tt.tty)
			ios.SetStderrTTY(tt.tty)
			browser := &browser.Stub{}
			opts.IO = ios
			opts.Browser = browser
			opts.HttpClient = func() (*http.Client, error) {
				return &http.Client{Transport: reg}, nil
			}
			opts.Config = func() (gh.Config, error) {
				return config.NewBlankConfig(), nil
			}
			opts.Remotes = func() (context.Remotes, error) {
				return context.Remotes{
					{
						Remote: &git.Remote{
							Name:     "origin",
							Resolved: "base",
						},
						Repo: ghrepo.New("OWNER", "REPO"),
					},
				}, nil
			}
			opts.Branch = func() (string, error) {
				return branch, nil
			}
			opts.Finder = shared.NewMockFinder(branch, nil, nil)
			opts.GitClient = &git.Client{
				GhPath:  "some/path/gh",
				GitPath: "some/path/git",
			}
			cleanSetup := func() {}
			if tt.setup != nil {
				cleanSetup = tt.setup(&opts, t)
			}
			defer cleanSetup()

			err := createRun(&opts)
			output := &test.CmdOut{
				OutBuf:     stdout,
				ErrBuf:     stderr,
				BrowsedURL: browser.BrowsedURL(),
			}
			if tt.wantErr != "" {
				assert.EqualError(t, err, tt.wantErr)
			} else {
				assert.NoError(t, err)
				if tt.expectedOut != "" {
					assert.Equal(t, tt.expectedOut, output.String())
				}
				if len(tt.expectedOutputs) > 0 {
					assert.Equal(t, tt.expectedOutputs, strings.Split(output.String(), "\n"))
				}
				assert.Equal(t, tt.expectedErrOut, output.Stderr())
				assert.Equal(t, tt.expectedBrowse, output.BrowsedURL)
			}
		})
	}
}

func Test_tryDetermineTrackingRef(t *testing.T) {
	tests := []struct {
		name                string
		cmdStubs            func(*run.CommandStubber)
		headBranchConfig    git.BranchConfig
		remotes             context.Remotes
		expectedTrackingRef trackingRef
		expectedFound       bool
	}{
		{
			name: "empty",
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD")
			},
			headBranchConfig:    git.BranchConfig{},
			expectedTrackingRef: trackingRef{},
			expectedFound:       false,
		},
		{
			name: "no match",
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register("git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature", 0, "abc HEAD\nbca refs/remotes/upstream/feature")
			},
			headBranchConfig: git.BranchConfig{},
			remotes: context.Remotes{
				&context.Remote{
					Remote: &git.Remote{Name: "upstream"},
					Repo:   ghrepo.New("octocat", "Spoon-Knife"),
				},
				&context.Remote{
					Remote: &git.Remote{Name: "origin"},
					Repo:   ghrepo.New("hubot", "Spoon-Knife"),
				},
			},
			expectedTrackingRef: trackingRef{},
			expectedFound:       false,
		},
		{
			name: "match",
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature$`, 0, heredoc.Doc(`
		deadbeef HEAD
		deadb00f refs/remotes/upstream/feature
		deadbeef refs/remotes/origin/feature
	`))
			},
			headBranchConfig: git.BranchConfig{},
			remotes: context.Remotes{
				&context.Remote{
					Remote: &git.Remote{Name: "upstream"},
					Repo:   ghrepo.New("octocat", "Spoon-Knife"),
				},
				&context.Remote{
					Remote: &git.Remote{Name: "origin"},
					Repo:   ghrepo.New("hubot", "Spoon-Knife"),
				},
			},
			expectedTrackingRef: trackingRef{
				remoteName: "origin",
				branchName: "feature",
			},
			expectedFound: true,
		},
		{
			name: "respect tracking config",
			cmdStubs: func(cs *run.CommandStubber) {
				cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/great-feat refs/remotes/origin/feature$`, 0, heredoc.Doc(`
		deadbeef HEAD
		deadb00f refs/remotes/origin/feature
	`))
			},
			headBranchConfig: git.BranchConfig{
				RemoteName: "origin",
				MergeRef:   "refs/heads/great-feat",
			},
			remotes: context.Remotes{
				&context.Remote{
					Remote: &git.Remote{Name: "origin"},
					Repo:   ghrepo.New("hubot", "Spoon-Knife"),
				},
			},
			expectedTrackingRef: trackingRef{},
			expectedFound:       false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cs, cmdTeardown := run.Stub()
			defer cmdTeardown(t)

			tt.cmdStubs(cs)

			gitClient := &git.Client{
				GhPath:  "some/path/gh",
				GitPath: "some/path/git",
			}

			ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", tt.headBranchConfig)

			assert.Equal(t, tt.expectedTrackingRef, ref)
			assert.Equal(t, tt.expectedFound, found)
		})
	}
}

func Test_generateCompareURL(t *testing.T) {
	tests := []struct {
		name    string
		ctx     CreateContext
		state   shared.IssueMetadataState
		want    string
		wantErr bool
	}{
		{
			name: "basic",
			ctx: CreateContext{
				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
				BaseBranch:      "main",
				HeadBranchLabel: "feature",
			},
			want:    "https://github.com/OWNER/REPO/compare/main...feature?body=&expand=1",
			wantErr: false,
		},
		{
			name: "with labels",
			ctx: CreateContext{
				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
				BaseBranch:      "a",
				HeadBranchLabel: "b",
			},
			state: shared.IssueMetadataState{
				Labels: []string{"one", "two three"},
			},
			want:    "https://github.com/OWNER/REPO/compare/a...b?body=&expand=1&labels=one%2Ctwo+three",
			wantErr: false,
		},
		{
			name: "'/'s in branch names/labels are percent-encoded",
			ctx: CreateContext{
				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
				BaseBranch:      "main/trunk",
				HeadBranchLabel: "owner:feature",
			},
			want:    "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner:feature?body=&expand=1",
			wantErr: false,
		},
		{
			name: "Any of !'(),; but none of $&+=@ and : in branch names/labels are percent-encoded ",
			/*
					- Technically, per section 3.3 of RFC 3986, none of !$&'()*+,;= (sub-delims) and :[]@ (part of gen-delims) in path segments are optionally percent-encoded, but url.PathEscape percent-encodes !'(),; anyway
					- !$&'()+,;=@ is a valid Git branch name—essentially RFC 3986 sub-delims without * and gen-delims without :/?#[]
					- : is GitHub separator between a fork name and a branch name
				    - See https://github.com/golang/go/issues/27559.
			*/
			ctx: CreateContext{
				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
				BaseBranch:      "main/trunk",
				HeadBranchLabel: "owner:!$&'()+,;=@",
			},
			want:    "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner:%21$&%27%28%29+%2C%3B=@?body=&expand=1",
			wantErr: false,
		},
		{
			name: "with template",
			ctx: CreateContext{
				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
				BaseBranch:      "main",
				HeadBranchLabel: "feature",
			},
			state: shared.IssueMetadataState{
				Template: "story.md",
			},
			want:    "https://github.com/OWNER/REPO/compare/main...feature?body=&expand=1&template=story.md",
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := generateCompareURL(tt.ctx, tt.state)
			if (err != nil) != tt.wantErr {
				t.Errorf("generateCompareURL() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("generateCompareURL() = %v, want %v", got, tt.want)
			}
		})
	}
}

func mockRetrieveProjects(_ *testing.T, reg *httpmock.Registry) {
	reg.Register(
		httpmock.GraphQL(`query RepositoryProjectList\b`),
		httpmock.StringResponse(`
				{ "data": { "repository": { "projects": {
					"nodes": [
						{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" },
						{ "name": "Roadmap", "id": "ROADMAPID", "resourcePath": "/OWNER/REPO/projects/2" }
					],
					"pageInfo": { "hasNextPage": false }
				} } } }
				`))
	reg.Register(
		httpmock.GraphQL(`query RepositoryProjectV2List\b`),
		httpmock.StringResponse(`
				{ "data": { "repository": { "projectsV2": {
					"nodes": [
						{ "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/3" },
						{ "title": "RoadmapV2", "id": "ROADMAPV2ID", "resourcePath": "/OWNER/REPO/projects/4" }
					],
					"pageInfo": { "hasNextPage": false }
				} } } }
				`))
	reg.Register(
		httpmock.GraphQL(`query OrganizationProjectList\b`),
		httpmock.StringResponse(`
				{ "data": { "organization": { "projects": {
					"nodes": [
						{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" }
					],
					"pageInfo": { "hasNextPage": false }
				} } } }
				`))
	reg.Register(
		httpmock.GraphQL(`query OrganizationProjectV2List\b`),
		httpmock.StringResponse(`
				{ "data": { "organization": { "projectsV2": {
					"nodes": [
						{ "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2" }
					],
					"pageInfo": { "hasNextPage": false }
				} } } }
				`))
	reg.Register(
		httpmock.GraphQL(`query UserProjectV2List\b`),
		httpmock.StringResponse(`
				{ "data": { "viewer": { "projectsV2": {
					"nodes": [
						{ "title": "MonalisaV2", "id": "MONALISAV2ID", "resourcePath": "/user/MONALISA/projects/2" }
					],
					"pageInfo": { "hasNextPage": false }
				} } } }
				`))
}

// TODO interactive metadata tests once: 1) we have test utils for Prompter and 2) metadata questions use Prompter
