package create

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

	"github.com/MakeNowJust/heredoc"
	"github.com/cli/cli/v2/internal/browser"
	"github.com/cli/cli/v2/internal/config"
	fd "github.com/cli/cli/v2/internal/featuredetection"
	"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"
	prShared "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:      "-t mytitle",
			wantsErr: true,
		},
		{
			name:     "empty tty",
			tty:      true,
			cli:      "",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:       "",
				Body:        "",
				RecoverFile: "",
				WebMode:     false,
				Interactive: true,
			},
		},
		{
			name:     "body from stdin",
			tty:      false,
			stdin:    "this is on standard input",
			cli:      "-t mytitle -F -",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:       "mytitle",
				Body:        "this is on standard input",
				RecoverFile: "",
				WebMode:     false,
				Interactive: false,
			},
		},
		{
			name:     "body from file",
			tty:      false,
			cli:      fmt.Sprintf("-t mytitle -F '%s'", tmpFile),
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:       "mytitle",
				Body:        "a body from file",
				RecoverFile: "",
				WebMode:     false,
				Interactive: false,
			},
		},
		{
			name:     "template from name tty",
			tty:      true,
			cli:      `-t mytitle --template "bug report"`,
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:       "mytitle",
				Body:        "",
				RecoverFile: "",
				WebMode:     false,
				Template:    "bug report",
				Interactive: true,
			},
		},
		{
			name:     "template from name non-tty",
			tty:      false,
			cli:      `-t mytitle --template "bug report"`,
			wantsErr: true,
		},
		{
			name:     "template and body",
			tty:      false,
			cli:      `-t mytitle --template "bug report" --body "issue body"`,
			wantsErr: true,
		},
		{
			name:     "template and body file",
			tty:      false,
			cli:      `-t mytitle --template "bug report" --body-file "body_file.md"`,
			wantsErr: true,
		},
		{
			name:     "editor by cli",
			tty:      true,
			cli:      "--editor",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:       "",
				Body:        "",
				RecoverFile: "",
				WebMode:     false,
				EditorMode:  true,
				Interactive: false,
			},
		},
		{
			name:     "editor by config",
			tty:      true,
			cli:      "",
			config:   "prefer_editor_prompt: enabled",
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:       "",
				Body:        "",
				RecoverFile: "",
				WebMode:     false,
				EditorMode:  true,
				Interactive: false,
			},
		},
		{
			name:     "editor and template",
			tty:      true,
			cli:      `--editor --template "bug report"`,
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:       "",
				Body:        "",
				RecoverFile: "",
				WebMode:     false,
				EditorMode:  true,
				Template:    "bug report",
				Interactive: false,
			},
		},
		{
			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",
				RecoverFile: "",
				WebMode:     true,
				EditorMode:  false,
				Interactive: false,
			},
		},
		{
			name:     "editor with non-tty",
			tty:      false,
			cli:      "--editor",
			wantsErr: true,
		},
		{
			name:     "type",
			tty:      false,
			cli:      `-t mytitle -b mybody --type Bug`,
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:     "mytitle",
				Body:      "mybody",
				IssueType: "Bug",
			},
		},
		{
			name:     "parent by number",
			tty:      false,
			cli:      `-t mytitle -b mybody --parent 100`,
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:  "mytitle",
				Body:   "mybody",
				Parent: "100",
			},
		},
		{
			name:     "parent by URL",
			tty:      false,
			cli:      `-t mytitle -b mybody --parent https://github.com/cli/go-gh/issues/42`,
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:  "mytitle",
				Body:   "mybody",
				Parent: "https://github.com/cli/go-gh/issues/42",
			},
		},
		{
			name:     "blocked by multiple issues",
			tty:      false,
			cli:      `-t mytitle -b mybody --blocked-by 200,201`,
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:     "mytitle",
				Body:      "mybody",
				BlockedBy: []string{"200", "201"},
			},
		},
		{
			name:     "blocking another issue",
			tty:      false,
			cli:      `-t mytitle -b mybody --blocking 300`,
			wantsErr: false,
			wantsOpts: CreateOptions{
				Title:    "mytitle",
				Body:     "mybody",
				Blocking: []string{"300"},
			},
		},
	}
	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(io.Discard)
			cmd.SetErr(io.Discard)
			_, 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.Title, opts.Title)
			assert.Equal(t, tt.wantsOpts.RecoverFile, opts.RecoverFile)
			assert.Equal(t, tt.wantsOpts.WebMode, opts.WebMode)
			assert.Equal(t, tt.wantsOpts.Interactive, opts.Interactive)
			assert.Equal(t, tt.wantsOpts.Template, opts.Template)
			assert.Equal(t, tt.wantsOpts.IssueType, opts.IssueType)
			assert.Equal(t, tt.wantsOpts.Parent, opts.Parent)
			assert.Equal(t, tt.wantsOpts.BlockedBy, opts.BlockedBy)
			assert.Equal(t, tt.wantsOpts.Blocking, opts.Blocking)
		})
	}
}

func Test_createRun(t *testing.T) {
	tests := []struct {
		name        string
		opts        CreateOptions
		httpStubs   func(*testing.T, *httpmock.Registry)
		promptStubs func(*prompter.PrompterMock)
		wantsStdout string
		wantsStderr string
		wantsBrowse string
		wantsErr    string
	}{
		{
			name: "no args",
			opts: CreateOptions{
				Detector: &fd.EnabledDetectorMock{},
				WebMode:  true,
			},
			wantsBrowse: "https://github.com/OWNER/REPO/issues/new",
			wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
		},
		{
			name: "title and body",
			opts: CreateOptions{
				Detector: &fd.EnabledDetectorMock{},
				WebMode:  true,
				Title:    "myissue",
				Body:     "hello cli",
			},
			wantsBrowse: "https://github.com/OWNER/REPO/issues/new?body=hello+cli&title=myissue",
			wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
		},
		{
			name: "assignee",
			opts: CreateOptions{
				Detector:  &fd.EnabledDetectorMock{},
				WebMode:   true,
				Assignees: []string{"monalisa"},
			},
			wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=monalisa&body=",
			wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
		},
		{
			name: "@me",
			opts: CreateOptions{
				Detector:  &fd.EnabledDetectorMock{},
				WebMode:   true,
				Assignees: []string{"@me"},
			},
			httpStubs: func(_ *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query UserCurrent\b`),
					httpmock.StringResponse(`
					{ "data": {
						"viewer": { "login": "MonaLisa" }
					} }`))
			},
			wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=MonaLisa&body=",
			wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
		},
		{
			name: "@copilot",
			opts: CreateOptions{
				Detector:  &fd.EnabledDetectorMock{},
				WebMode:   true,
				Assignees: []string{"@copilot"},
			},
			wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=Copilot&body=",
			wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
		},
		{
			name: "project",
			opts: CreateOptions{
				Detector: &fd.EnabledDetectorMock{},
				WebMode:  true,
				Projects: []string{"cleanup"},
			},
			httpStubs: func(_ *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query RepositoryProjectList\b`),
					httpmock.StringResponse(`
					{ "data": { "repository": { "projects": {
						"nodes": [
							{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" }
						],
						"pageInfo": { "hasNextPage": false }
					} } } }`))
				r.Register(
					httpmock.GraphQL(`query OrganizationProjectList\b`),
					httpmock.StringResponse(`
					{ "data": { "organization": { "projects": {
						"nodes": [
							{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1"  }
						],
						"pageInfo": { "hasNextPage": false }
					} } } }`))
				r.Register(
					httpmock.GraphQL(`query RepositoryProjectV2List\b`),
					httpmock.StringResponse(`
					{ "data": { "repository": { "projectsV2": {
						"nodes": [
							{ "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/2" }
						],
						"pageInfo": { "hasNextPage": false }
					} } } }`))
				r.Register(
					httpmock.GraphQL(`query OrganizationProjectV2List\b`),
					httpmock.StringResponse(`
					{ "data": { "organization": { "projectsV2": {
						"nodes": [
							{ "title": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/2"  }
						],
						"pageInfo": { "hasNextPage": false }
					} } } }`))
				r.Register(
					httpmock.GraphQL(`query UserProjectV2List\b`),
					httpmock.StringResponse(`
					{ "data": { "viewer": { "projectsV2": {
						"nodes": [
							{ "title": "Monalisa", "id": "MONALISAID", "resourcePath": "/users/MONALISA/projects/1"  }
						],
						"pageInfo": { "hasNextPage": false }
					} } } }`))
			},
			wantsBrowse: "https://github.com/OWNER/REPO/issues/new?body=&projects=OWNER%2FREPO%2F1",
			wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
		},
		{
			name: "has templates",
			opts: CreateOptions{
				Detector: &fd.EnabledDetectorMock{},
				WebMode:  true,
			},
			httpStubs: func(_ *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query IssueTemplates\b`),
					httpmock.StringResponse(`
					{ "data": { "repository": { "issueTemplates": [
						{ "name": "Bug report",
							"body": "Does not work :((" },
						{ "name": "Submit a request",
							"body": "I have a suggestion for an enhancement" }
					] } } }`),
				)
			},
			wantsBrowse: "https://github.com/OWNER/REPO/issues/new/choose",
			wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new/choose in your browser.\n",
		},
		{
			name: "too long body",
			opts: CreateOptions{
				Detector: &fd.EnabledDetectorMock{},
				WebMode:  true,
				Body:     strings.Repeat("A", 9216),
			},
			wantsErr: "cannot open in browser: maximum URL length exceeded",
		},
		{
			name: "editor",
			httpStubs: func(_ *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query IssueRepositoryInfo\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": {
							"id": "REPOID",
							"hasIssuesEnabled": true
						} } }`))
				r.Register(
					httpmock.GraphQL(`mutation IssueCreate\b`),
					httpmock.GraphQLMutation(`
						{ "data": { "createIssue": { "issue": {
							"URL": "https://github.com/OWNER/REPO/issues/12"
						} } } }
					`, func(inputs map[string]interface{}) {
						assert.Equal(t, "title", inputs["title"])
						assert.Equal(t, "body", inputs["body"])
					}))
			},
			opts: CreateOptions{
				Detector:         &fd.EnabledDetectorMock{},
				EditorMode:       true,
				TitledEditSurvey: func(string, string) (string, string, error) { return "title", "body", nil },
			},
			wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
			wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
		},
		{
			name: "editor and template",
			httpStubs: func(_ *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query IssueRepositoryInfo\b`),
					httpmock.StringResponse(`
			{ "data": { "repository": {
				"id": "REPOID",
				"hasIssuesEnabled": true
			} } }`))
				r.Register(
					httpmock.GraphQL(`query IssueTemplates\b`),
					httpmock.StringResponse(`
			{ "data": { "repository": { "issueTemplates": [
				{ "name": "Bug report",
				  "title": "bug: ",
				  "body": "Does not work :((" }
			] } } }`),
				)
				r.Register(
					httpmock.GraphQL(`mutation IssueCreate\b`),
					httpmock.GraphQLMutation(`
		{ "data": { "createIssue": { "issue": {
			"URL": "https://github.com/OWNER/REPO/issues/12"
		} } } }
	`, func(inputs map[string]interface{}) {
						assert.Equal(t, "bug: ", inputs["title"])
						assert.Equal(t, "Does not work :((", inputs["body"])
					}))
			},
			opts: CreateOptions{
				Detector:         &fd.EnabledDetectorMock{},
				EditorMode:       true,
				Template:         "Bug report",
				TitledEditSurvey: func(title string, body string) (string, string, error) { return title, body, nil },
			},
			wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
			wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
		},
		{
			name: "interactive prompts with actor assignee display names when actors available",
			opts: CreateOptions{
				Interactive: true,
				Detector:    &fd.EnabledDetectorMock{},
				Title:       "test `gh issue create` actor assignees",
				Body:        "Actor assignees allow users and bots to be assigned to issues",
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				firstConfirmSubmission := true
				pm.InputFunc = func(message, defaultValue string) (string, error) {
					switch message {
					default:
						return "", fmt.Errorf("unexpected input prompt: %s", message)
					}
				}
				pm.MultiSelectFunc = func(message string, defaults []string, options []string) ([]int, error) {
					switch message {
					case "What would you like to add?":
						return prompter.IndexesFor(options, "Assignees")
					default:
						return nil, fmt.Errorf("unexpected multi-select prompt: %s", message)
					}
				}
				pm.MultiSelectWithSearchFunc = func(message, searchPrompt string, defaults, persistentOptions []string, searchFunc func(string) prompter.MultiSelectSearchResult) ([]string, error) {
					switch message {
					case "Assignees":
						return []string{"copilot-swe-agent", "MonaLisa"}, nil
					default:
						return nil, fmt.Errorf("unexpected multi-select-with-search prompt: %s", message)
					}
				}
				pm.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
					switch message {
					case "What's next?":
						if firstConfirmSubmission {
							firstConfirmSubmission = false
							return prompter.IndexFor(options, "Add metadata")
						}
						return prompter.IndexFor(options, "Submit")
					default:
						return 0, fmt.Errorf("unexpected select prompt: %s", message)
					}
				}
			},
			httpStubs: func(_ *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query IssueRepositoryInfo\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": {
							"id": "REPOID",
							"hasIssuesEnabled": true,
							"viewerPermission": "WRITE"
						} } }
					`))
				r.Register(
					httpmock.GraphQL(`mutation IssueCreate\b`),
					httpmock.GraphQLMutation(`
						{ "data": { "createIssue": { "issue": {
							"id": "ISSUEID",
							"URL": "https://github.com/OWNER/REPO/issues/12"
						} } } }
					`, func(inputs map[string]interface{}) {
						if v, ok := inputs["assigneeIds"]; ok {
							t.Errorf("did not expect assigneeIds: %v", v)
						}
					}))
				r.Register(
					httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
					httpmock.GraphQLMutation(`
						{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }
					`, func(inputs map[string]interface{}) {
						assert.Equal(t, "ISSUEID", inputs["assignableId"])
						assert.Equal(t, []interface{}{"copilot-swe-agent[bot]", "MonaLisa"}, inputs["actorLogins"])
					}))
			},
			wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
			wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
		},
		{
			name: "interactive prompts with user assignee logins when actors unavailable",
			opts: CreateOptions{
				Interactive: true,
				Detector:    &fd.DisabledDetectorMock{},
				Title:       "test `gh issue create` user assignees",
				Body:        "User assignees allow only users to be assigned to issues",
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				firstConfirmSubmission := true
				pm.InputFunc = func(message, defaultValue string) (string, error) {
					switch message {
					default:
						return "", fmt.Errorf("unexpected input prompt: %s", message)
					}
				}
				pm.MultiSelectFunc = func(message string, defaults []string, options []string) ([]int, error) {
					switch message {
					case "What would you like to add?":
						return prompter.IndexesFor(options, "Assignees")
					case "Assignees":
						return prompter.IndexesFor(options, "hubot", "MonaLisa (Mona Display Name)")
					default:
						return nil, fmt.Errorf("unexpected multi-select prompt: %s", message)
					}
				}
				pm.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
					switch message {
					case "What's next?":
						if firstConfirmSubmission {
							firstConfirmSubmission = false
							return prompter.IndexFor(options, "Add metadata")
						}
						return prompter.IndexFor(options, "Submit")
					default:
						return 0, fmt.Errorf("unexpected select prompt: %s", message)
					}
				}
			},
			httpStubs: func(_ *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query IssueRepositoryInfo\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": {
							"id": "REPOID",
							"hasIssuesEnabled": true,
							"viewerPermission": "WRITE"
						} } }
					`))
				r.Register(
					httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": { "assignableUsers": {
							"nodes": [
								{ "login": "hubot", "id": "HUBOTID", "name": "" },
								{ "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name" }
							],
							"pageInfo": { "hasNextPage": false }
						} } } }
					`))
				r.Register(
					httpmock.GraphQL(`mutation IssueCreate\b`),
					httpmock.GraphQLMutation(`
						{ "data": { "createIssue": { "issue": {
							"URL": "https://github.com/OWNER/REPO/issues/12"
						} } } }
					`, func(inputs map[string]interface{}) {
						assert.Equal(t, []interface{}{"HUBOTID", "MONAID"}, inputs["assigneeIds"])
					}))
			},
			wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
			wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
		},
		{
			name: "create with type",
			opts: CreateOptions{
				Detector:  &fd.EnabledDetectorMock{},
				Title:     "bug title",
				Body:      "bug body",
				IssueType: "Bug",
			},
			httpStubs: func(t *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query IssueRepositoryInfo\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": {
							"id": "REPOID",
							"hasIssuesEnabled": true
						} } }`))
				r.Register(
					httpmock.GraphQL(`mutation IssueCreate\b`),
					httpmock.StringResponse(`
						{ "data": { "createIssue": { "issue": {
							"id": "ISSUE_ID_123",
							"URL": "https://github.com/OWNER/REPO/issues/123"
						} } } }`))
				r.Register(
					httpmock.GraphQL(`query RepositoryIssueTypes\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": { "issueTypes": { "nodes": [
							{ "id": "IT_1", "name": "Bug", "description": "", "color": "d73a4a" },
							{ "id": "IT_2", "name": "Feature", "description": "", "color": "0075ca" },
							{ "id": "IT_3", "name": "Task", "description": "", "color": "e4e669" }
						] } } } }`))
				r.Register(
					httpmock.GraphQL(`mutation UpdateIssueIssueType\b`),
					httpmock.GraphQLMutation(`
						{ "data": { "updateIssueIssueType": { "issue": { "id": "ISSUE_ID_123" } } } }`,
						func(inputs map[string]interface{}) {
							assert.Equal(t, "ISSUE_ID_123", inputs["issueId"])
							assert.Equal(t, "IT_1", inputs["issueTypeId"])
						}))
			},
			wantsStdout: "https://github.com/OWNER/REPO/issues/123\n",
			wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
		},
		{
			name: "interactive prompts for type",
			opts: CreateOptions{
				Interactive: true,
				Detector:    &fd.EnabledDetectorMock{},
				Title:       "feature request",
				Body:        "would be nice to have",
			},
			promptStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
					switch message {
					case "Issue type":
						return prompter.IndexFor(options, "Feature")
					case "What's next?":
						return prompter.IndexFor(options, "Submit")
					default:
						return 0, fmt.Errorf("unexpected select prompt: %s", message)
					}
				}
			},
			httpStubs: func(t *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query IssueRepositoryInfo\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": {
							"id": "REPOID",
							"hasIssuesEnabled": true,
							"viewerPermission": "WRITE"
						} } }`))
				// Issue types are fetched up front to power the interactive prompt.
				r.Register(
					httpmock.GraphQL(`query RepositoryIssueTypes\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": { "issueTypes": { "nodes": [
							{ "id": "IT_1", "name": "Bug", "description": "", "color": "d73a4a" },
							{ "id": "IT_2", "name": "Feature", "description": "", "color": "0075ca" },
							{ "id": "IT_3", "name": "Task", "description": "", "color": "e4e669" }
						] } } } }`))
				r.Register(
					httpmock.GraphQL(`mutation IssueCreate\b`),
					httpmock.StringResponse(`
						{ "data": { "createIssue": { "issue": {
							"id": "ISSUE_ID_123",
							"URL": "https://github.com/OWNER/REPO/issues/123"
						} } } }`))
				// Selected ID is reused without re-fetching the types list.
				r.Register(
					httpmock.GraphQL(`mutation UpdateIssueIssueType\b`),
					httpmock.GraphQLMutation(`
						{ "data": { "updateIssueIssueType": { "issue": { "id": "ISSUE_ID_123" } } } }`,
						func(inputs map[string]interface{}) {
							assert.Equal(t, "ISSUE_ID_123", inputs["issueId"])
							assert.Equal(t, "IT_2", inputs["issueTypeId"])
						}))
			},
			wantsStdout: "https://github.com/OWNER/REPO/issues/123\n",
			wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
		},
		{
			name: "create with type not found",
			opts: CreateOptions{
				Detector:  &fd.EnabledDetectorMock{},
				Title:     "bug title",
				Body:      "bug body",
				IssueType: "Bugz",
			},
			httpStubs: func(_ *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query IssueRepositoryInfo\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": {
							"id": "REPOID",
							"hasIssuesEnabled": true
						} } }`))
				r.Register(
					httpmock.GraphQL(`mutation IssueCreate\b`),
					httpmock.StringResponse(`
						{ "data": { "createIssue": { "issue": {
							"id": "ISSUE_ID_123",
							"URL": "https://github.com/OWNER/REPO/issues/123"
						} } } }`))
				r.Register(
					httpmock.GraphQL(`query RepositoryIssueTypes\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": { "issueTypes": { "nodes": [
							{ "id": "IT_1", "name": "Bug", "description": "", "color": "d73a4a" },
							{ "id": "IT_2", "name": "Feature", "description": "", "color": "0075ca" },
							{ "id": "IT_3", "name": "Task", "description": "", "color": "e4e669" }
						] } } } }`))
			},
			wantsErr: `type "Bugz" not found; available types: Bug, Feature, Task`,
		},
		{
			name: "create with parent",
			opts: CreateOptions{
				Detector: &fd.EnabledDetectorMock{},
				Title:    "child issue",
				Body:     "child body",
				Parent:   "100",
			},
			httpStubs: func(t *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query IssueRepositoryInfo\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": {
							"id": "REPOID",
							"hasIssuesEnabled": true
						} } }`))
				r.Register(
					httpmock.GraphQL(`mutation IssueCreate\b`),
					httpmock.StringResponse(`
						{ "data": { "createIssue": { "issue": {
							"id": "ISSUE_ID_123",
							"URL": "https://github.com/OWNER/REPO/issues/123"
						} } } }`))
				r.Register(
					httpmock.GraphQL(`query IssueNodeID\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": { "issue": { "id": "PARENT_ID_100" } } } }`))
				r.Register(
					httpmock.GraphQL(`mutation AddSubIssue\b`),
					httpmock.GraphQLMutation(`
						{ "data": { "addSubIssue": { "issue": { "id": "PARENT_ID_100" } } } }`,
						func(inputs map[string]interface{}) {
							assert.Equal(t, "PARENT_ID_100", inputs["issueId"])
							assert.Equal(t, "ISSUE_ID_123", inputs["subIssueId"])
							assert.Equal(t, false, inputs["replaceParent"])
						}))
			},
			wantsStdout: "https://github.com/OWNER/REPO/issues/123\n",
			wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
		},
		{
			name: "create with blocked-by and blocking",
			opts: CreateOptions{
				Detector:  &fd.EnabledDetectorMock{},
				Title:     "blocked issue",
				Body:      "blocked body",
				BlockedBy: []string{"200", "201"},
				Blocking:  []string{"300", "301"},
			},
			httpStubs: func(t *testing.T, r *httpmock.Registry) {
				r.Register(
					httpmock.GraphQL(`query IssueRepositoryInfo\b`),
					httpmock.StringResponse(`
						{ "data": { "repository": {
							"id": "REPOID",
							"hasIssuesEnabled": true
						} } }`))
				r.Register(
					httpmock.GraphQL(`mutation IssueCreate\b`),
					httpmock.StringResponse(`
						{ "data": { "createIssue": { "issue": {
							"id": "ISSUE_ID_123",
							"URL": "https://github.com/OWNER/REPO/issues/123"
						} } } }`))
				// IssueNodeID lookups for each ref, routed by number so they
				// don't depend on parallel ordering.
				r.Register(
					issueNodeIDByNumberMatcher(200),
					httpmock.StringResponse(`{ "data": { "repository": { "issue": { "id": "BLOCKER_ID_200" } } } }`))
				r.Register(
					issueNodeIDByNumberMatcher(201),
					httpmock.StringResponse(`{ "data": { "repository": { "issue": { "id": "BLOCKER_ID_201" } } } }`))
				r.Register(
					issueNodeIDByNumberMatcher(300),
					httpmock.StringResponse(`{ "data": { "repository": { "issue": { "id": "BLOCKED_ID_300" } } } }`))
				r.Register(
					issueNodeIDByNumberMatcher(301),
					httpmock.StringResponse(`{ "data": { "repository": { "issue": { "id": "BLOCKED_ID_301" } } } }`))
				// AddBlockedBy mutations, routed by their inputs so they
				// also don't depend on parallel ordering.
				// --blocked-by N: this issue is blocked by N
				r.Register(
					httpmock.GraphQLMutationMatcher(`mutation AddBlockedBy\b`, func(input map[string]interface{}) bool {
						return input["issueId"] == "ISSUE_ID_123" && input["blockingIssueId"] == "BLOCKER_ID_200"
					}),
					httpmock.StringResponse(`{ "data": { "addBlockedBy": { "issue": { "id": "ISSUE_ID_123" } } } }`))
				r.Register(
					httpmock.GraphQLMutationMatcher(`mutation AddBlockedBy\b`, func(input map[string]interface{}) bool {
						return input["issueId"] == "ISSUE_ID_123" && input["blockingIssueId"] == "BLOCKER_ID_201"
					}),
					httpmock.StringResponse(`{ "data": { "addBlockedBy": { "issue": { "id": "ISSUE_ID_123" } } } }`))
				// --blocking N: N is blocked by this issue (args swapped)
				r.Register(
					httpmock.GraphQLMutationMatcher(`mutation AddBlockedBy\b`, func(input map[string]interface{}) bool {
						return input["issueId"] == "BLOCKED_ID_300" && input["blockingIssueId"] == "ISSUE_ID_123"
					}),
					httpmock.StringResponse(`{ "data": { "addBlockedBy": { "issue": { "id": "BLOCKED_ID_300" } } } }`))
				r.Register(
					httpmock.GraphQLMutationMatcher(`mutation AddBlockedBy\b`, func(input map[string]interface{}) bool {
						return input["issueId"] == "BLOCKED_ID_301" && input["blockingIssueId"] == "ISSUE_ID_123"
					}),
					httpmock.StringResponse(`{ "data": { "addBlockedBy": { "issue": { "id": "BLOCKED_ID_301" } } } }`))
			},
			wantsStdout: "https://github.com/OWNER/REPO/issues/123\n",
			wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			httpReg := &httpmock.Registry{}
			defer httpReg.Verify(t)
			if tt.httpStubs != nil {
				tt.httpStubs(t, httpReg)
			}

			ios, _, stdout, stderr := iostreams.Test()
			ios.SetStdoutTTY(true)
			opts := &tt.opts
			opts.IO = ios
			opts.HttpClient = func() (*http.Client, error) {
				return &http.Client{Transport: httpReg}, nil
			}
			opts.BaseRepo = func() (ghrepo.Interface, error) {
				return ghrepo.New("OWNER", "REPO"), nil
			}
			browser := &browser.Stub{}
			opts.Browser = browser

			prompterMock := &prompter.PrompterMock{}
			opts.Prompter = prompterMock
			if tt.promptStubs != nil {
				tt.promptStubs(prompterMock)
			}

			err := createRun(opts)
			if tt.wantsErr == "" {
				require.NoError(t, err)
			} else {
				assert.EqualError(t, err, tt.wantsErr)
				return
			}

			assert.Equal(t, tt.wantsStdout, stdout.String())
			assert.Equal(t, tt.wantsStderr, stderr.String())
			browser.Verify(t, tt.wantsBrowse)
		})
	}
}

/*** LEGACY TESTS ***/

func runCommand(rt http.RoundTripper, isTTY bool, cli string, pm *prompter.PrompterMock) (*test.CmdOut, error) {
	return runCommandWithRootDirOverridden(rt, isTTY, cli, "", pm)
}

func runCommandWithRootDirOverridden(rt http.RoundTripper, isTTY bool, cli string, rootDir string, pm *prompter.PrompterMock) (*test.CmdOut, error) {
	ios, _, stdout, stderr := iostreams.Test()
	ios.SetStdoutTTY(isTTY)
	ios.SetStdinTTY(isTTY)
	ios.SetStderrTTY(isTTY)

	browser := &browser.Stub{}
	factory := &cmdutil.Factory{
		IOStreams: ios,
		HttpClient: func() (*http.Client, error) {
			return &http.Client{Transport: rt}, nil
		},
		Config: func() (gh.Config, error) {
			return config.NewBlankConfig(), nil
		},
		BaseRepo: func() (ghrepo.Interface, error) {
			return ghrepo.New("OWNER", "REPO"), nil
		},
		Browser:  browser,
		Prompter: pm,
	}

	cmd := NewCmdCreate(factory, func(opts *CreateOptions) error {
		opts.RootDirOverride = rootDir
		opts.Detector = &fd.EnabledDetectorMock{}
		return createRun(opts)
	})

	argv, err := shlex.Split(cli)
	if err != nil {
		return nil, err
	}
	cmd.SetArgs(argv)

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

	_, err = cmd.ExecuteC()
	return &test.CmdOut{
		OutBuf:     stdout,
		ErrBuf:     stderr,
		BrowsedURL: browser.BrowsedURL(),
	}, err
}

func TestIssueCreate(t *testing.T) {
	http := &httpmock.Registry{}
	defer http.Verify(t)

	http.Register(
		httpmock.GraphQL(`query IssueRepositoryInfo\b`),
		httpmock.StringResponse(`
			{ "data": { "repository": {
				"id": "REPOID",
				"hasIssuesEnabled": true
			} } }`),
	)
	http.Register(
		httpmock.GraphQL(`mutation IssueCreate\b`),
		httpmock.GraphQLMutation(`
				{ "data": { "createIssue": { "issue": {
					"URL": "https://github.com/OWNER/REPO/issues/12"
				} } } }`,
			func(inputs map[string]interface{}) {
				assert.Equal(t, inputs["repositoryId"], "REPOID")
				assert.Equal(t, inputs["title"], "hello")
				assert.Equal(t, inputs["body"], "cash rules everything around me")
			}),
	)

	output, err := runCommand(http, true, `-t hello -b "cash rules everything around me"`, nil)
	if err != nil {
		t.Errorf("error running command `issue create`: %v", err)
	}

	assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}

func TestIssueCreate_recover(t *testing.T) {
	http := &httpmock.Registry{}
	defer http.Verify(t)

	http.Register(
		httpmock.GraphQL(`query IssueRepositoryInfo\b`),
		httpmock.StringResponse(`
			{ "data": { "repository": {
				"id": "REPOID",
				"hasIssuesEnabled": true
			} } }`))
	// Should only be one fetch of metadata.
	http.Register(
		httpmock.GraphQL(`query RepositoryLabelList\b`),
		httpmock.StringResponse(`
			{ "data": { "repository": { "labels": {
				"nodes": [
					{ "name": "TODO", "id": "TODOID" },
					{ "name": "bug", "id": "BUGID" }
				],
				"pageInfo": { "hasNextPage": false }
			} } } }
		`))
	http.Register(
		httpmock.GraphQL(`mutation IssueCreate\b`),
		httpmock.GraphQLMutation(`
		{ "data": { "createIssue": { "issue": {
			"URL": "https://github.com/OWNER/REPO/issues/12"
		} } } }
	`, func(inputs map[string]interface{}) {
			assert.Equal(t, "recovered title", inputs["title"])
			assert.Equal(t, "recovered body", inputs["body"])
			assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
		}))

	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 prompter.IndexFor(opts, "Submit")
		} else {
			return -1, prompter.NoSuchPromptErr(p)
		}
	}

	tmpfile, err := os.CreateTemp(t.TempDir(), "testrecover*")
	assert.NoError(t, err)
	defer tmpfile.Close()

	state := prShared.IssueMetadataState{
		Title:  "recovered title",
		Body:   "recovered body",
		Labels: []string{"bug", "TODO"},
	}

	data, err := json.Marshal(state)
	assert.NoError(t, err)

	_, err = tmpfile.Write(data)
	assert.NoError(t, err)

	args := fmt.Sprintf("--recover '%s'", tmpfile.Name())

	output, err := runCommandWithRootDirOverridden(http, true, args, "", pm)
	if err != nil {
		t.Errorf("error running command `issue create`: %v", err)
	}

	assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}

func TestIssueCreate_nonLegacyTemplate(t *testing.T) {
	http := &httpmock.Registry{}
	defer http.Verify(t)

	http.Register(
		httpmock.GraphQL(`query IssueRepositoryInfo\b`),
		httpmock.StringResponse(`
			{ "data": { "repository": {
				"id": "REPOID",
				"hasIssuesEnabled": true
			} } }`),
	)
	http.Register(
		httpmock.GraphQL(`query IssueTemplates\b`),
		httpmock.StringResponse(`
			{ "data": { "repository": { "issueTemplates": [
				{ "name": "Bug report",
				  "body": "Does not work :((" },
				{ "name": "Submit a request",
				  "body": "I have a suggestion for an enhancement" }
			] } } }`),
	)
	http.Register(
		httpmock.GraphQL(`mutation IssueCreate\b`),
		httpmock.GraphQLMutation(`
			{ "data": { "createIssue": { "issue": {
				"URL": "https://github.com/OWNER/REPO/issues/12"
			} } } }`,
			func(inputs map[string]interface{}) {
				assert.Equal(t, inputs["repositoryId"], "REPOID")
				assert.Equal(t, inputs["title"], "hello")
				assert.Equal(t, inputs["body"], "I have a suggestion for an enhancement")
			}),
	)

	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")
		case "Choose a template":
			return prompter.IndexFor(opts, "Submit a request")
		default:
			return -1, prompter.NoSuchPromptErr(p)
		}
	}

	output, err := runCommandWithRootDirOverridden(http, true, `-t hello`, "./fixtures/repoWithNonLegacyIssueTemplates", pm)
	if err != nil {
		t.Errorf("error running command `issue create`: %v", err)
	}

	assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
	assert.Equal(t, "", output.BrowsedURL)
}

func TestIssueCreate_continueInBrowser(t *testing.T) {
	http := &httpmock.Registry{}
	defer http.Verify(t)

	http.Register(
		httpmock.GraphQL(`query IssueRepositoryInfo\b`),
		httpmock.StringResponse(`
			{ "data": { "repository": {
				"id": "REPOID",
				"hasIssuesEnabled": true
			} } }`),
	)

	pm := &prompter.PrompterMock{}
	pm.InputFunc = func(p, d string) (string, error) {
		if p == "Title (required)" {
			return "hello", nil
		} else {
			return "", prompter.NoSuchPromptErr(p)
		}
	}
	pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
		if p == "What's next?" {
			return prompter.IndexFor(opts, "Continue in browser")
		} else {
			return -1, prompter.NoSuchPromptErr(p)
		}
	}

	_, cmdTeardown := run.Stub()
	defer cmdTeardown(t)

	output, err := runCommand(http, true, `-b body`, pm)
	if err != nil {
		t.Errorf("error running command `issue create`: %v", err)
	}

	assert.Equal(t, "", output.String())
	assert.Equal(t, heredoc.Doc(`

		Creating issue in OWNER/REPO

		Opening https://github.com/OWNER/REPO/issues/new in your browser.
	`), output.Stderr())
	assert.Equal(t, "https://github.com/OWNER/REPO/issues/new?body=body&title=hello", output.BrowsedURL)
}

func TestIssueCreate_metadata(t *testing.T) {
	http := &httpmock.Registry{}
	defer http.Verify(t)

	http.StubIssueRepoInfoResponse("OWNER", "REPO")
	http.Register(
		httpmock.GraphQL(`query RepositoryLabelList\b`),
		httpmock.StringResponse(`
		{ "data": { "repository": { "labels": {
			"nodes": [
				{ "name": "TODO", "id": "TODOID" },
				{ "name": "bug", "id": "BUGID" }
			],
			"pageInfo": { "hasNextPage": false }
		} } } }
		`))
	http.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 }
		} } } }
		`))
	http.Register(
		httpmock.GraphQL(`query RepositoryProjectList\b`),
		httpmock.StringResponse(`
		{ "data": { "repository": { "projects": {
			"nodes": [
				{ "name": "Cleanup", "id": "CLEANUPID" },
				{ "name": "Roadmap", "id": "ROADMAPID" }
			],
			"pageInfo": { "hasNextPage": false }
		} } } }
		`))
	http.Register(
		httpmock.GraphQL(`query OrganizationProjectList\b`),
		httpmock.StringResponse(`
		{	"data": { "organization": null },
			"errors": [{
				"type": "NOT_FOUND",
				"path": [ "organization" ],
				"message": "Could not resolve to an Organization with the login of 'OWNER'."
			}]
		}
		`))
	http.Register(
		httpmock.GraphQL(`query RepositoryProjectV2List\b`),
		httpmock.StringResponse(`
		{ "data": { "repository": { "projectsV2": {
			"nodes": [],
			"pageInfo": { "hasNextPage": false }
		} } } }
		`))
	http.Register(
		httpmock.GraphQL(`query OrganizationProjectV2List\b`),
		httpmock.StringResponse(`
		{	"data": { "organization": { "projectsV2": {
			"nodes": [],
			"pageInfo": { "hasNextPage": false }
		} } } }
		`))
	http.Register(
		httpmock.GraphQL(`query UserProjectV2List\b`),
		httpmock.StringResponse(`
		{	"data": { "viewer": { "projectsV2": {
			"nodes": [],
			"pageInfo": { "hasNextPage": false }
		} } } }
		`))
	http.Register(
		httpmock.GraphQL(`mutation IssueCreate\b`),
		httpmock.GraphQLMutation(`
		{ "data": { "createIssue": { "issue": {
			"id": "NEWISSUEID",
			"URL": "https://github.com/OWNER/REPO/issues/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)
			}
			assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
			assert.Equal(t, []interface{}{"ROADMAPID"}, inputs["projectIds"])
			assert.Equal(t, "BIGONEID", inputs["milestoneId"])
			assert.NotContains(t, inputs, "userIds")
			assert.NotContains(t, inputs, "teamIds")
			assert.NotContains(t, inputs, "projectV2Ids")
		}))
	http.Register(
		httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
		httpmock.GraphQLMutation(`
		{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }
	`, func(inputs map[string]interface{}) {
			assert.Equal(t, "NEWISSUEID", inputs["assignableId"])
			assert.Equal(t, []interface{}{"monalisa"}, inputs["actorLogins"])
		}))

	output, err := runCommand(http, true, `-t TITLE -b BODY -a monalisa -l bug -l todo -p roadmap -m 'big one.oh'`, nil)
	if err != nil {
		t.Errorf("error running command `issue create`: %v", err)
	}

	assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}

func TestIssueCreate_disabledIssues(t *testing.T) {
	http := &httpmock.Registry{}
	defer http.Verify(t)

	http.Register(
		httpmock.GraphQL(`query IssueRepositoryInfo\b`),
		httpmock.StringResponse(`
			{ "data": { "repository": {
				"id": "REPOID",
				"hasIssuesEnabled": false
			} } }`),
	)

	_, err := runCommand(http, true, `-t heres -b johnny`, nil)
	if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
		t.Errorf("error running command `issue create`: %v", err)
	}
}

func TestIssueCreate_AtMeAssignee(t *testing.T) {
	http := &httpmock.Registry{}
	defer http.Verify(t)

	http.Register(
		httpmock.GraphQL(`query UserCurrent\b`),
		httpmock.StringResponse(`
		{ "data": {
			"viewer": { "login": "MonaLisa" }
		} }
		`),
	)
	http.Register(
		httpmock.GraphQL(`query IssueRepositoryInfo\b`),
		httpmock.StringResponse(`
		{ "data": { "repository": {
			"id": "REPOID",
			"hasIssuesEnabled": true
		} } }
	`))
	http.Register(
		httpmock.GraphQL(`mutation IssueCreate\b`),
		httpmock.GraphQLMutation(`
		{ "data": { "createIssue": { "issue": {
			"id": "NEWISSUEID",
			"URL": "https://github.com/OWNER/REPO/issues/12"
		} } } }
	`, func(inputs map[string]interface{}) {
			assert.Equal(t, "hello", inputs["title"])
			assert.Equal(t, "cash rules everything around me", inputs["body"])
			if v, ok := inputs["assigneeIds"]; ok {
				t.Errorf("did not expect assigneeIds: %v", v)
			}
		}))
	http.Register(
		httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
		httpmock.GraphQLMutation(`
		{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }
	`, func(inputs map[string]interface{}) {
			assert.Equal(t, "NEWISSUEID", inputs["assignableId"])
			assert.Equal(t, []interface{}{"MonaLisa", "someoneelse"}, inputs["actorLogins"])
		}))

	output, err := runCommand(http, true, `-a @me -a someoneelse -t hello -b "cash rules everything around me"`, nil)
	if err != nil {
		t.Errorf("error running command `issue create`: %v", err)
	}

	assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}

func TestIssueCreate_AtCopilotAssignee(t *testing.T) {
	http := &httpmock.Registry{}
	defer http.Verify(t)

	http.Register(
		httpmock.GraphQL(`query IssueRepositoryInfo\b`),
		httpmock.StringResponse(`
		{ "data": { "repository": {
			"id": "REPOID",
			"hasIssuesEnabled": true
		} } }
	`))
	http.Register(
		httpmock.GraphQL(`mutation IssueCreate\b`),
		httpmock.GraphQLMutation(`
		{ "data": { "createIssue": { "issue": {
			"id": "NEWISSUEID",
			"URL": "https://github.com/OWNER/REPO/issues/12"
		} } } }
	`, func(inputs map[string]interface{}) {
			assert.Equal(t, "hello", inputs["title"])
			assert.Equal(t, "cash rules everything around me", inputs["body"])
			if v, ok := inputs["assigneeIds"]; ok {
				t.Errorf("did not expect assigneeIds: %v", v)
			}
		}))
	http.Register(
		httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
		httpmock.GraphQLMutation(`
		{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }
	`, func(inputs map[string]interface{}) {
			assert.Equal(t, "NEWISSUEID", inputs["assignableId"])
			assert.Equal(t, []interface{}{"copilot-swe-agent[bot]"}, inputs["actorLogins"])
		}))

	output, err := runCommand(http, true, `-a @copilot -t hello -b "cash rules everything around me"`, nil)
	if err != nil {
		t.Errorf("error running command `issue create`: %v", err)
	}

	assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}

func TestIssueCreate_projectsV2(t *testing.T) {
	http := &httpmock.Registry{}
	defer http.Verify(t)

	http.StubIssueRepoInfoResponse("OWNER", "REPO")
	http.Register(
		httpmock.GraphQL(`query RepositoryProjectList\b`),
		httpmock.StringResponse(`
		{ "data": { "repository": { "projects": {
			"nodes": [],
			"pageInfo": { "hasNextPage": false }
		} } } }
		`))
	http.Register(
		httpmock.GraphQL(`query OrganizationProjectList\b`),
		httpmock.StringResponse(`
		{	"data": { "organization": { "projects": {
			"nodes": [],
			"pageInfo": { "hasNextPage": false }
		} } } }
		`))
	http.Register(
		httpmock.GraphQL(`query RepositoryProjectV2List\b`),
		httpmock.StringResponse(`
		{ "data": { "repository": { "projectsV2": {
			"nodes": [
				{ "title": "CleanupV2", "id": "CLEANUPV2ID" },
				{ "title": "RoadmapV2", "id": "ROADMAPV2ID" }
			],
			"pageInfo": { "hasNextPage": false }
		} } } }
		`))
	http.Register(
		httpmock.GraphQL(`query OrganizationProjectV2List\b`),
		httpmock.StringResponse(`
		{ "data": { "organization": { "projectsV2": {
			"nodes": [
				{ "title": "TriageV2", "id": "TRIAGEV2ID" }
			],
			"pageInfo": { "hasNextPage": false }
		} } } }
		`))
	http.Register(
		httpmock.GraphQL(`query UserProjectV2List\b`),
		httpmock.StringResponse(`
		{ "data": { "viewer": { "projectsV2": {
			"nodes": [
				{ "title": "MonalisaV2", "id": "MONALISAV2ID" }
			],
			"pageInfo": { "hasNextPage": false }
		} } } }
		`))
	http.Register(
		httpmock.GraphQL(`mutation IssueCreate\b`),
		httpmock.GraphQLMutation(`
		{ "data": { "createIssue": { "issue": {
			"id": "Issue#1",
			"URL": "https://github.com/OWNER/REPO/issues/12"
		} } } }
	`, func(inputs map[string]interface{}) {
			assert.Equal(t, "TITLE", inputs["title"])
			assert.Equal(t, "BODY", inputs["body"])
			assert.Nil(t, inputs["projectIds"])
			assert.NotContains(t, inputs, "projectV2Ids")
		}))
	http.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":"Issue#1","projectId":"ROADMAPV2ID"}}`
			assert.Equal(t, expectedMutations, mutations)
			assert.Equal(t, expectedVariables, string(variables))
		}))

	output, err := runCommand(http, true, `-t TITLE -b BODY -p roadmapv2`, nil)
	if err != nil {
		t.Errorf("error running command `issue create`: %v", err)
	}

	assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}

// TODO projectsV1Deprecation
// Remove this test.
func TestProjectsV1Deprecation(t *testing.T) {

	t.Run("non-interactive submission", func(t *testing.T) {
		t.Run("when projects v1 is supported, queries for it", func(t *testing.T) {
			ios, _, _, _ := iostreams.Test()

			reg := &httpmock.Registry{}
			reg.StubIssueRepoInfoResponse("OWNER", "REPO")
			reg.Register(
				// ( is required to avoid matching projectsV2
				httpmock.GraphQL(`projects\(`),
				// Simulate a GraphQL error to early exit the test.
				httpmock.StatusStringResponse(500, ""),
			)

			_, cmdTeardown := run.Stub()
			defer cmdTeardown(t)

			// Ignore the error because we have no way to really stub it without
			// fully stubbing a GQL error structure in the request body.
			_ = createRun(&CreateOptions{
				IO: ios,
				HttpClient: func() (*http.Client, error) {
					return &http.Client{Transport: reg}, nil
				},
				BaseRepo: func() (ghrepo.Interface, error) {
					return ghrepo.New("OWNER", "REPO"), nil
				},

				Detector: &fd.EnabledDetectorMock{},
				Title:    "Test Title",
				Body:     "Test Body",
				// Required to force a lookup of projects
				Projects: []string{"Project"},
			})

			// Verify that our request contained projects
			reg.Verify(t)
		})

		t.Run("when projects v1 is not supported, does not query for it", func(t *testing.T) {
			ios, _, _, _ := iostreams.Test()

			reg := &httpmock.Registry{}
			reg.StubIssueRepoInfoResponse("OWNER", "REPO")
			// ( is required to avoid matching projectsV2
			reg.Exclude(t, httpmock.GraphQL(`projects\(`))

			_, cmdTeardown := run.Stub()
			defer cmdTeardown(t)

			// Ignore the error because we're not really interested in it.
			_ = createRun(&CreateOptions{
				IO: ios,
				HttpClient: func() (*http.Client, error) {
					return &http.Client{Transport: reg}, nil
				},
				BaseRepo: func() (ghrepo.Interface, error) {
					return ghrepo.New("OWNER", "REPO"), nil
				},

				Detector: &fd.DisabledDetectorMock{},
				Title:    "Test Title",
				Body:     "Test Body",
				// Required to force a lookup of projects
				Projects: []string{"Project"},
			})

			// Verify that our request contained projectCards
			reg.Verify(t)
		})
	})

	t.Run("web mode", func(t *testing.T) {
		t.Run("when projects v1 is supported, queries for it", func(t *testing.T) {
			ios, _, _, _ := iostreams.Test()

			reg := &httpmock.Registry{}
			reg.Register(
				// ( is required to avoid matching projectsV2
				httpmock.GraphQL(`projects\(`),
				// Simulate a GraphQL error to early exit the test.
				httpmock.StatusStringResponse(500, ""),
			)

			_, cmdTeardown := run.Stub()
			defer cmdTeardown(t)

			// Ignore the error because we have no way to really stub it without
			// fully stubbing a GQL error structure in the request body.
			_ = createRun(&CreateOptions{
				IO: ios,
				HttpClient: func() (*http.Client, error) {
					return &http.Client{Transport: reg}, nil
				},
				BaseRepo: func() (ghrepo.Interface, error) {
					return ghrepo.New("OWNER", "REPO"), nil
				},

				Detector: &fd.EnabledDetectorMock{},
				WebMode:  true,
				// Required to force a lookup of projects
				Projects: []string{"Project"},
			})

			// Verify that our request contained projects
			reg.Verify(t)
		})

		t.Run("when projects v1 is not supported, does not query for it", func(t *testing.T) {
			ios, _, _, _ := iostreams.Test()

			reg := &httpmock.Registry{}
			// ( is required to avoid matching projectsV2
			reg.Exclude(t, httpmock.GraphQL(`projects\(`))

			_, cmdTeardown := run.Stub()
			defer cmdTeardown(t)

			// Ignore the error because we're not really interested in it.
			_ = createRun(&CreateOptions{
				IO: ios,
				HttpClient: func() (*http.Client, error) {
					return &http.Client{Transport: reg}, nil
				},
				BaseRepo: func() (ghrepo.Interface, error) {
					return ghrepo.New("OWNER", "REPO"), nil
				},

				Detector: &fd.DisabledDetectorMock{},
				WebMode:  true,
				// Required to force a lookup of projects
				Projects: []string{"Project"},
			})

			// Verify that our request contained projectCards
			reg.Verify(t)
		})
	})
}

// issueNodeIDByNumberMatcher matches an IssueNodeID GraphQL query whose
// number variable equals the given value. Used by tests that issue
// multiple IssueNodeID lookups and need stubs to route by issue number
// rather than by registration order.
func issueNodeIDByNumberMatcher(number int) httpmock.Matcher {
	queryMatcher := httpmock.GraphQL(`query IssueNodeID\b`)
	return func(req *http.Request) bool {
		if !queryMatcher(req) {
			return false
		}
		body, err := io.ReadAll(req.Body)
		if err != nil {
			return false
		}
		req.Body = io.NopCloser(bytes.NewReader(body))
		var b struct {
			Variables struct {
				Number int `json:"number"`
			} `json:"variables"`
		}
		if err := json.Unmarshal(body, &b); err != nil {
			return false
		}
		return b.Variables.Number == number
	}
}
