package cmdutils

import (
	"errors"
	"fmt"
	"testing"

	"gitlab.com/gitlab-org/cli/pkg/iostreams"

	"github.com/acarl005/stripansi"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/xanzy/go-gitlab"
	"gitlab.com/gitlab-org/cli/api"
	"gitlab.com/gitlab-org/cli/internal/glrepo"
	"gitlab.com/gitlab-org/cli/pkg/git"
	"gitlab.com/gitlab-org/cli/pkg/prompt"
)

func Test_ParseAssignees(t *testing.T) {
	testCases := []struct {
		name        string
		input       []string
		wantAdd     []string
		wantRemove  []string
		wantReplace []string
	}{
		{
			name:        "simple replace",
			input:       []string{"foo"},
			wantAdd:     []string{},
			wantRemove:  []string{},
			wantReplace: []string{"foo"},
		},
		{
			name:        "only add",
			input:       []string{"+foo"},
			wantAdd:     []string{"foo"},
			wantRemove:  []string{},
			wantReplace: []string{},
		},
		{
			name:        "only remove",
			input:       []string{"-foo", "!bar"},
			wantAdd:     []string{},
			wantRemove:  []string{"foo", "bar"},
			wantReplace: []string{},
		},
		{
			name:        "only replace",
			input:       []string{"baz"},
			wantAdd:     []string{},
			wantRemove:  []string{},
			wantReplace: []string{"baz"},
		},
		{
			name:        "add and remove",
			input:       []string{"+qux", "-foo", "!bar"},
			wantAdd:     []string{"qux"},
			wantRemove:  []string{"foo", "bar"},
			wantReplace: []string{},
		},
		{
			name:        "add and replace",
			input:       []string{"+foo", "bar"},
			wantAdd:     []string{"foo"},
			wantRemove:  []string{},
			wantReplace: []string{"bar"},
		},
		{
			name:        "remove and replace",
			input:       []string{"-foo", "bar", "!baz"},
			wantAdd:     []string{},
			wantRemove:  []string{"foo", "baz"},
			wantReplace: []string{"bar"},
		},
	}
	for _, tC := range testCases {
		t.Run(tC.name, func(t *testing.T) {
			uaGot := ParseAssignees(tC.input)
			assert.ElementsMatch(t, uaGot.ToAdd, tC.wantAdd)
			assert.ElementsMatch(t, uaGot.ToRemove, tC.wantRemove)
			assert.ElementsMatch(t, uaGot.ToReplace, tC.wantReplace)
		})
	}
}

func Test_VerifyAssignees(t *testing.T) {
	testCases := []struct {
		name  string
		input UserAssignments
		want  string // expected error message
	}{
		{
			name: "empty, no errors",
			input: UserAssignments{
				ToAdd:     []string{},
				ToRemove:  []string{},
				ToReplace: []string{},
			},
		},
		{
			name: "simple addition, no errors",
			input: UserAssignments{
				ToAdd:     []string{"foo"},
				ToRemove:  []string{},
				ToReplace: []string{},
			},
		},
		{
			name: "simple removal, no errors",
			input: UserAssignments{
				ToAdd:     []string{},
				ToRemove:  []string{"foo"},
				ToReplace: []string{},
			},
		},
		{
			name: "simple replace, no errors",
			input: UserAssignments{
				ToAdd:     []string{},
				ToRemove:  []string{},
				ToReplace: []string{"foo"},
			},
		},
		{
			name: "add and removal with multiple elements, no errors",
			input: UserAssignments{
				ToAdd:     []string{"foo", "bar", "baz"},
				ToRemove:  []string{"qux", "quux", "quz"},
				ToReplace: []string{},
			},
		},
		{
			name: "multi replace, no errors",
			input: UserAssignments{
				ToAdd:     []string{},
				ToRemove:  []string{},
				ToReplace: []string{"foo", "bar"},
			},
		},
		{
			name: "replace with add, error",
			input: UserAssignments{
				ToAdd:     []string{"bar"},
				ToRemove:  []string{},
				ToReplace: []string{"foo"},
			},
			want: "mixing relative (+,!,-) and absolute assignments is forbidden.",
		},
		{
			name: "replace with remove, error",
			input: UserAssignments{
				ToAdd:     []string{},
				ToRemove:  []string{"baz"},
				ToReplace: []string{"foo"},
			},
			want: "mixing relative (+,!,-) and absolute assignments is forbidden.",
		},
		{
			name: "overlapping add and removal element, error",
			input: UserAssignments{
				ToAdd:     []string{"foo"},
				ToRemove:  []string{"foo"},
				ToReplace: []string{},
			},
			want: `1 element "foo" present in both add and remove, which is forbidden.`,
		},
		{
			name: "overlapping add and removal elements, error",
			input: UserAssignments{
				ToAdd:     []string{"foo", "bar", "baz"},
				ToRemove:  []string{"foo", "baz"},
				ToReplace: []string{},
			},
			want: `2 elements "foo baz" present in both add and remove, which is forbidden.`,
		},
	}
	for _, tC := range testCases {
		t.Run(tC.name, func(t *testing.T) {
			err := tC.input.VerifyAssignees()
			if tC.want == "" {
				if err != nil {
					t.Errorf("VerifyAssignees() unexpected error = %s", err)
				}
			} else {
				if tC.want != err.Error() {
					t.Errorf("VerifyAssignees() expected = %s, got = %s", tC.want, err.Error())
				}
			}
		})
	}
}

func Test_UsersFromReplaces(t *testing.T) {
	testCases := []struct {
		name           string
		users          []*gitlab.User
		expectedIDs    []int
		expectedAction []string
	}{
		{
			name:           "nothingness",
			users:          []*gitlab.User{},
			expectedIDs:    []int{},
			expectedAction: []string{},
		},
		{
			name: "single user named foo",
			users: []*gitlab.User{
				{ID: 1, Username: "foo"},
			},
			expectedIDs:    []int{1},
			expectedAction: []string{`assigned to "@foo"`},
		},
		{
			name: "multiple users named foo, bar and baz",
			users: []*gitlab.User{
				{ID: 1, Username: "foo"},
				{ID: 3, Username: "bar"},
				{ID: 7, Username: "baz"},
			},
			expectedIDs:    []int{1, 3, 7},
			expectedAction: []string{`assigned to "@foo @bar @baz"`},
		},
	}
	for _, tC := range testCases {
		t.Run(tC.name, func(t *testing.T) {
			ua := UserAssignments{}
			api.UsersByNames = func(apiClient *gitlab.Client, names []string) ([]*gitlab.User, error) {
				return tC.users, nil
			}
			var gotAction []string
			gotIDs, gotAction, err := ua.UsersFromReplaces(&gitlab.Client{}, gotAction)
			if err != nil {
				t.Errorf("UsersFromReplaces() unexpected error = %s", err)
			}
			assert.ElementsMatch(t, *gotIDs, tC.expectedIDs)
			assert.ElementsMatch(t, gotAction, tC.expectedAction)
		})
	}
}

func Test_UserAssignmentsAPIFailure(t *testing.T) {
	want := "failed to get users by their names" // Error message we want
	ua := UserAssignments{
		ToAdd: []string{"foo"},
	} // Fill `ToAdd` so `cmdutils.UsersFromAddRemove()` reaches the api call
	var err error

	api.UsersByNames = func(apiClient *gitlab.Client, names []string) ([]*gitlab.User, error) {
		return nil, fmt.Errorf("failed to get users by their names")
	}

	apiClient := gitlab.Client{} // Empty Client, it won't be used, just to satisfy the function signature
	_, _, err = ua.UsersFromReplaces(&apiClient, nil)
	if err == nil {
		t.Errorf("UsersFromReplaces() expected error to not be nil")
	}
	if want != err.Error() {
		t.Errorf("UsersFromReplace() expected error = %s, got = %v", want, err)
	}

	_, _, err = ua.UsersFromAddRemove(nil, nil, &apiClient, nil)
	if err == nil {
		t.Errorf("UsersFromReplaces() expected error to not be nil")
	}
	if want != err.Error() {
		t.Errorf("UsersFromReplace() expected error = %s, got = %v", want, err)
	}
}

func Test_UsersFromAddRemove(t *testing.T) {
	testCases := []struct {
		name           string
		users          []*gitlab.User          // Mock *gitlab.User received from api.UsersByNames
		merge          []*gitlab.BasicUser     // Mock `.Assignee field` from a merge request
		issue          []*gitlab.IssueAssignee // Mock `.Assignee field` from an issue
		expectedIDs    []int
		expectedAction []string
		ua             UserAssignments
		wantErr        string
	}{
		{
			name: "add foo (issue and merge request)",
			users: []*gitlab.User{
				{
					ID:       1,
					Username: "foo",
				},
			},
			expectedIDs:    []int{1},
			expectedAction: []string{`assigned "@foo"`},
			ua:             UserAssignments{ToAdd: []string{"foo"}},
		},
		{
			name: "add foo, bar and baz (issue and merge request)",
			users: []*gitlab.User{
				{
					ID:       1,
					Username: "foo",
				},
				{
					ID:       235,
					Username: "bar",
				},
				{
					ID:       1500,
					Username: "baz",
				},
			},
			expectedIDs:    []int{1, 235, 1500},
			expectedAction: []string{`assigned "@foo @bar @baz"`},
			ua:             UserAssignments{ToAdd: []string{"foo", "bar", "baz"}},
		},
		{
			name:  "remove foo (issue)",
			users: []*gitlab.User{},
			issue: []*gitlab.IssueAssignee{
				{
					ID:       1,
					Username: "foo",
				},
			},
			expectedIDs:    []int{0},
			expectedAction: []string{`unassigned "@foo"`},
			ua:             UserAssignments{ToRemove: []string{"foo"}},
		},
		{
			name:  "remove foo and baz out of foo, bar and baz (issue)",
			users: []*gitlab.User{},
			issue: []*gitlab.IssueAssignee{
				{
					ID:       1,
					Username: "foo",
				},
				{
					ID:       2,
					Username: "bar",
				},
				{
					ID:       3,
					Username: "baz",
				},
			},
			expectedIDs:    []int{2},
			expectedAction: []string{`unassigned "@foo @baz"`},
			ua:             UserAssignments{ToRemove: []string{"foo", "baz"}},
		},
		{
			name: "remove foo out of foo and baz and add bar (issue)",
			users: []*gitlab.User{
				{
					ID:       100,
					Username: "bar",
				},
			},
			issue: []*gitlab.IssueAssignee{
				{
					ID:       1,
					Username: "foo",
				},
				{
					ID:       500,
					Username: "baz",
				},
			},
			expectedIDs: []int{500, 100},
			expectedAction: []string{
				`unassigned "@foo"`,
				`assigned "@bar"`,
			},
			ua: UserAssignments{
				ToAdd:    []string{"bar"},
				ToRemove: []string{"foo"},
			},
		},
		{
			name:  "remove foo (merge request)",
			users: []*gitlab.User{},
			merge: []*gitlab.BasicUser{
				{
					ID:       1,
					Username: "foo",
				},
			},
			expectedIDs:    []int{0},
			expectedAction: []string{`unassigned "@foo"`},
			ua:             UserAssignments{ToRemove: []string{"foo"}},
		},
		{
			name:  "remove foo and baz out of foo, bar and baz (merge request)",
			users: []*gitlab.User{},
			merge: []*gitlab.BasicUser{
				{
					ID:       1,
					Username: "foo",
				},
				{
					ID:       2,
					Username: "bar",
				},
				{
					ID:       3,
					Username: "baz",
				},
			},
			expectedIDs:    []int{2},
			expectedAction: []string{`unassigned "@foo @baz"`},
			ua:             UserAssignments{ToRemove: []string{"foo", "baz"}},
		},
		{
			name: "remove foo out of foo and baz and add bar (merge request)",
			users: []*gitlab.User{
				{
					ID:       100,
					Username: "bar",
				},
			},
			merge: []*gitlab.BasicUser{
				{
					ID:       1,
					Username: "foo",
				},
				{
					ID:       500,
					Username: "baz",
				},
			},
			expectedIDs: []int{500, 100},
			expectedAction: []string{
				`unassigned "@foo"`,
				`assigned "@bar"`,
			},
			ua: UserAssignments{
				ToAdd:    []string{"bar"},
				ToRemove: []string{"foo"},
			},
		},
		{
			name: "try to pass both issue and merge request users",
			issue: []*gitlab.IssueAssignee{
				{
					ID:       1,
					Username: "foo",
				},
			},
			merge: []*gitlab.BasicUser{
				{
					ID:       5,
					Username: "bar",
				},
			},
			wantErr: "issueAssignees and mergeRequestAssignees can't both be set.",
		},
	}
	for _, tC := range testCases {
		t.Run(tC.name, func(t *testing.T) {
			api.UsersByNames = func(_ *gitlab.Client, _ []string) ([]*gitlab.User, error) {
				return tC.users, nil
			}
			var gotAction []string
			gotIDs, gotAction, err := tC.ua.UsersFromAddRemove(tC.issue, tC.merge, &gitlab.Client{}, gotAction)
			if err != nil {
				if tC.wantErr != "" && tC.wantErr != err.Error() {
					t.Errorf("UsersFromAddRemove() expected error = %s, got = %s", tC.wantErr, err)
				} else if tC.wantErr == "" {
					t.Errorf("UsersFromAddRemove() unexpected error = %s", err)
				}
			}

			assert.ElementsMatch(t, *gotIDs, tC.expectedIDs)
			assert.ElementsMatch(t, gotAction, tC.expectedAction)
		})
	}
}

func Test_ParseMilestoneTitleIsID(t *testing.T) {
	title := "1"
	expectedMilestoneID := 1

	// Override function to return an error, it should never reach this
	api.ProjectMilestoneByTitle = func(client *gitlab.Client, projectID interface{}, name string) (*gitlab.Milestone, error) {
		return nil, fmt.Errorf("We shouldn't have reached here")
	}

	got, err := ParseMilestone(&gitlab.Client{}, glrepo.New("foo", "bar"), title)
	if err != nil {
		t.Errorf("ParseMilestone() unexpected error = %s", err)
	}
	if got != expectedMilestoneID {
		t.Errorf("ParseMilestone() got = %d, expected = %d", got, expectedMilestoneID)
	}
}

func Test_ParseMilestoneAPIFail(t *testing.T) {
	title := "AsLongAsItDoesn'tConvertToInt"
	want := "API call failed in api.MilestoneByTitle()."

	// Override function to return an error simulating an API call failure
	api.ProjectMilestoneByTitle = func(client *gitlab.Client, projectID interface{}, name string) (*gitlab.Milestone, error) {
		return nil, fmt.Errorf("API call failed in api.MilestoneByTitle().")
	}

	_, err := ParseMilestone(&gitlab.Client{}, glrepo.New("foo", "bar"), title)
	if err == nil {
		t.Errorf("ParseMilestone() expected error")
	}
	if want != err.Error() {
		t.Errorf("ParseMilestone() expected error = %s, got error = %s", want, err)
	}
}

func Test_ParseMilestoneTitleToID(t *testing.T) {
	milestoneTitle := "kind: testing"
	expectedID := 3

	// Override function so it returns the correct milestone
	api.ProjectMilestoneByTitle = func(client *gitlab.Client, projectID interface{}, name string) (*gitlab.Milestone, error) {
		return &gitlab.Milestone{
				Title: "kind: testing",
				ID:    3,
			},
			nil
	}

	got, err := ParseMilestone(&gitlab.Client{}, glrepo.New("foo", "bar"), milestoneTitle)
	if err != nil {
		t.Errorf("ParseMilestone() unexpected error = %s", err)
	}
	if got != expectedID {
		t.Errorf("ParseMilestone() expected = %d, got = %d", expectedID, got)
	}
}

func Test_PickMetadata(t *testing.T) {
	const (
		labelsLabel    = "labels"
		assigneeLabel  = "assignees"
		milestoneLabel = "milestones"
	)

	testCases := []struct {
		name     string
		values   []string
		expected []Action
	}{
		{
			name: "nothing picked",
		},
		{
			name:     "labels",
			values:   []string{labelsLabel},
			expected: []Action{AddLabelAction},
		},
		{
			name:     "assignees",
			values:   []string{assigneeLabel},
			expected: []Action{AddAssigneeAction},
		},
		{
			name:     "milestone",
			values:   []string{milestoneLabel},
			expected: []Action{AddMilestoneAction},
		},
		{
			name:     "labels and assignees",
			values:   []string{labelsLabel, assigneeLabel},
			expected: []Action{AddLabelAction, AddAssigneeAction},
		},
		{
			name:     "labels and milestone",
			values:   []string{labelsLabel, milestoneLabel},
			expected: []Action{AddLabelAction, AddMilestoneAction},
		},
		{
			name:     "assignees and milestone",
			values:   []string{assigneeLabel, milestoneLabel},
			expected: []Action{AddAssigneeAction, AddMilestoneAction},
		},
		{
			name:     "labels, assignees and milestone",
			values:   []string{labelsLabel, assigneeLabel, milestoneLabel},
			expected: []Action{AddLabelAction, AddAssigneeAction, AddMilestoneAction},
		},
	}
	for _, tC := range testCases {
		t.Run(tC.name, func(t *testing.T) {
			as, restoreAsk := prompt.InitAskStubber()
			defer restoreAsk()

			as.Stub([]*prompt.QuestionStub{
				{
					Name:  "metadata",
					Value: tC.values,
				},
			})

			got, err := PickMetadata()
			if err != nil {
				t.Errorf("PickMetadata() unexpected error = %s", err)
			}
			assert.ElementsMatch(t, got, tC.expected)
		})
	}

	t.Run("Prompt fails", func(t *testing.T) {
		as, restoreAsk := prompt.InitAskStubber()
		defer restoreAsk()

		as.Stub([]*prompt.QuestionStub{
			{
				Name:  "metadata",
				Value: errors.New("meant to fail"),
			},
		})

		got, err := PickMetadata()
		assert.Nil(t, got)
		assert.EqualError(t, err, "could not prompt: meant to fail")
	})
}

func Test_UsersPrompt(t *testing.T) {
	// mock glrepo.Remote object
	repo := glrepo.New("foo", "bar")
	remote := &git.Remote{
		Name:     "test",
		Resolved: "base",
	}
	repoRemote := &glrepo.Remote{
		Remote: remote,
		Repo:   repo,
	}

	testCases := []struct {
		name               string
		choice             []string
		mock               []*gitlab.ProjectMember
		output             []string
		minimumAccessLevel int
		expectedStdErr     string
		expectedError      string
	}{
		{
			name: "nothing",
		},
		{
			name:               "reporter",
			choice:             []string{"foo (reporter)"},
			output:             []string{"foo"},
			minimumAccessLevel: 20,
			mock: []*gitlab.ProjectMember{
				{
					Username:    "foo",
					AccessLevel: gitlab.AccessLevelValue(20),
				},
			},
		},
		{
			name:               "reporter-developer",
			choice:             []string{"foo (reporter)", "bar (developer)"},
			output:             []string{"foo", "bar"},
			minimumAccessLevel: 20,
			mock: []*gitlab.ProjectMember{
				{
					Username:    "foo",
					AccessLevel: gitlab.AccessLevelValue(20),
				},
				{
					Username:    "bar",
					AccessLevel: gitlab.AccessLevelValue(30),
				},
			},
		},
		{
			name:               "reporter-developer-maintainer",
			choice:             []string{"foo (reporter)", "bar (developer)", "baz (maintainer)"},
			output:             []string{"foo", "bar", "baz"},
			minimumAccessLevel: 20,
			mock: []*gitlab.ProjectMember{
				{
					Username:    "foo",
					AccessLevel: gitlab.AccessLevelValue(20),
				},
				{
					Username:    "bar",
					AccessLevel: gitlab.AccessLevelValue(30),
				},
				{
					Username:    "baz",
					AccessLevel: gitlab.AccessLevelValue(40),
				},
			},
		},
		{
			name:               "no-members",
			minimumAccessLevel: 10,
			mock:               []*gitlab.ProjectMember{},
			expectedStdErr:     "Couldn't fetch any members with minimum permission level 10.\n",
		},
		{
			name:               "no-valid-members",
			minimumAccessLevel: 50,
			mock: []*gitlab.ProjectMember{
				{
					Username:    "foo",
					AccessLevel: gitlab.AccessLevelValue(40),
				},
			},
			expectedStdErr: "Couldn't fetch any members with minimum permission level 50.\n",
		},
	}
	for _, tC := range testCases {
		t.Run(tC.name, func(t *testing.T) {
			api.ListProjectMembers = func(client *gitlab.Client, projectID interface{}, opts *gitlab.ListProjectMembersOptions) ([]*gitlab.ProjectMember, error) {
				return tC.mock, nil
			}

			as, restoreAsk := prompt.InitAskStubber()
			defer restoreAsk()

			as.Stub([]*prompt.QuestionStub{
				{
					Name:  "some users",
					Value: tC.choice,
				},
			})

			var got []string
			io, _, _, stderr := iostreams.Test()

			err := UsersPrompt(&got, &gitlab.Client{}, repoRemote, io, tC.minimumAccessLevel, "some users")
			if tC.expectedError != "" {
				assert.EqualError(t, err, tC.expectedError)
			} else {
				assert.NoError(t, err)
			}
			if tC.expectedStdErr != "" {
				outErr := stripansi.Strip(stderr.String())

				assert.Equal(t, tC.expectedStdErr, outErr)
			}
			assert.ElementsMatch(t, got, tC.output)
		})
	}

	t.Run("Prompt fails", func(t *testing.T) {
		var got []string

		api.ListProjectMembers = func(client *gitlab.Client, projectID interface{}, opts *gitlab.ListProjectMembersOptions) ([]*gitlab.ProjectMember, error) {
			return []*gitlab.ProjectMember{
				{
					Username:    "foo",
					AccessLevel: gitlab.AccessLevelValue(20),
				},
			}, nil
		}

		as, restoreAsk := prompt.InitAskStubber()
		defer restoreAsk()

		as.Stub([]*prompt.QuestionStub{
			{
				Name:  "assignees",
				Value: errors.New("meant to fail"),
			},
		})

		err := UsersPrompt(&got, &gitlab.Client{}, repoRemote, nil, 20, "assignees")
		assert.Empty(t, got)
		assert.EqualError(t, err, "meant to fail")
	})

	t.Run("API Failed", func(t *testing.T) {
		var got []string

		api.ListProjectMembers = func(client *gitlab.Client, projectID interface{}, opts *gitlab.ListProjectMembersOptions) ([]*gitlab.ProjectMember, error) {
			return nil, errors.New("meant to fail")
		}

		err := UsersPrompt(&got, &gitlab.Client{}, repoRemote, nil, 20, "assignees")
		assert.Empty(t, got)
		assert.EqualError(t, err, "meant to fail")
	})

	t.Run("respect-flags", func(t *testing.T) {
		got := []string{"foo"}

		api.ListProjectMembers = func(client *gitlab.Client, projectID interface{}, opts *gitlab.ListProjectMembersOptions) ([]*gitlab.ProjectMember, error) {
			return []*gitlab.ProjectMember{
				{
					Username:    "foo",
					AccessLevel: gitlab.AccessLevelValue(20),
				},
				{
					Username:    "bar",
					AccessLevel: gitlab.AccessLevelValue(30),
				},
			}, nil
		}

		as, restoreAsk := prompt.InitAskStubber()
		defer restoreAsk()

		as.Stub([]*prompt.QuestionStub{
			{
				Name:  "assignees",
				Value: []string{"bar (developer)"},
			},
		})

		err := UsersPrompt(&got, &gitlab.Client{}, repoRemote, nil, 20, "assignees")
		assert.NoError(t, err)
		assert.ElementsMatch(t, []string{"foo", "bar"}, got)
	})
}

func Test_MilestonesPrompt(t *testing.T) {
	mockMilestones := []*api.Milestone{
		{
			Title: "New Release",
			ID:    5,
		},
		{
			Title: "Really big feature",
			ID:    240,
		},
		{
			Title: "Get rid of low quality Code",
			ID:    650,
		},
	}

	// Override API.ListMilestones so it doesn't make any network calls
	api.ListAllMilestones = func(_ *gitlab.Client, _ interface{}, _ *api.ListMilestonesOptions) ([]*api.Milestone, error) {
		return mockMilestones, nil
	}

	// mock glrepo.Remote object
	repo := glrepo.New("foo", "bar")
	remote := &git.Remote{
		Name:     "test",
		Resolved: "base",
	}
	repoRemote := &glrepo.Remote{
		Remote: remote,
		Repo:   repo,
	}

	testCases := []struct {
		name       string
		input      string // Selected milestone
		expectedID int    // expected global ID from the milestone
	}{
		{
			name:       "match",
			input:      "New Release",
			expectedID: 5,
		},
	}
	for _, tC := range testCases {
		t.Run(tC.name, func(t *testing.T) {
			as, restoreAsk := prompt.InitAskStubber()
			defer restoreAsk()

			as.Stub([]*prompt.QuestionStub{
				{
					Name:  "milestone",
					Value: tC.input,
				},
			})

			var got int
			var io iostreams.IOStreams

			err := MilestonesPrompt(&got, &gitlab.Client{}, repoRemote, &io)
			if err != nil {
				t.Errorf("MilestonesPrompt() unexpected error = %s", err)
			}
			if got != 0 && got != tC.expectedID {
				t.Errorf("MilestonesPrompt() expected = %d, got = %d", got, tC.expectedID)
			}
		})
	}

	t.Run("Prompt fails", func(t *testing.T) {
		as, restoreAsk := prompt.InitAskStubber()
		defer restoreAsk()

		as.Stub([]*prompt.QuestionStub{
			{
				Name:  "milestone",
				Value: errors.New("meant to fail"),
			},
		})

		var got int
		var io iostreams.IOStreams

		err := MilestonesPrompt(&got, &gitlab.Client{}, repoRemote, &io)
		assert.Empty(t, got)
		assert.EqualError(t, err, "meant to fail")
	})
}

func Test_MilestonesPromptNoPrompts(t *testing.T) {
	// Override api.ListMilestones so it returns an empty slice, we are testing if MilestonesPrompt()
	// will print the correct message to `stderr` when it tries to get the list of Milestones in a
	// project but the project has no milestones
	api.ListAllMilestones = func(_ *gitlab.Client, _ interface{}, _ *api.ListMilestonesOptions) ([]*api.Milestone, error) {
		return []*api.Milestone{}, nil
	}

	// mock glrepo.Remote object
	repo := glrepo.New("foo", "bar")
	remote := &git.Remote{
		Name:     "test",
		Resolved: "base",
	}
	repoRemote := &glrepo.Remote{
		Remote: remote,
		Repo:   repo,
	}

	var got int
	io, _, _, stderr := iostreams.Test()

	err := MilestonesPrompt(&got, &gitlab.Client{}, repoRemote, io)
	if err != nil {
		t.Errorf("MilestonesPrompt() unexpected error = %s", err)
	}
	assert.Equal(t, "No active milestones exist for this project.\n", stderr.String())
}

func TestMilestonesPromptFailures(t *testing.T) {
	// Override api.ListMilestones so it returns an error, we are testing to see if error
	// handling from the usage of api.ListMilestones is correct
	api.ListAllMilestones = func(_ *gitlab.Client, _ interface{}, _ *api.ListMilestonesOptions) ([]*api.Milestone, error) {
		return nil, errors.New("api.ListMilestones() failed")
	}

	// mock glrepo.Remote object
	repo := glrepo.New("foo", "bar")
	remote := &git.Remote{
		Name:     "test",
		Resolved: "base",
	}
	repoRemote := &glrepo.Remote{
		Remote: remote,
		Repo:   repo,
	}

	var got int
	io, _, _, _ := iostreams.Test()

	err := MilestonesPrompt(&got, &gitlab.Client{}, repoRemote, io)
	if err == nil {
		t.Error("MilestonesPrompt() expected error")
	}
	assert.Equal(t, "api.ListMilestones() failed", err.Error())
}

func Test_IDsFromUsers(t *testing.T) {
	testCases := []struct {
		name  string
		users []*gitlab.User // Mock of the gitlab.User object
		IDs   []int          // IDs we expect from the users
	}{
		{
			name: "no users",
		},
		{
			name: "one user",
			users: []*gitlab.User{
				{
					ID: 1,
				},
			},
			IDs: []int{1},
		},
		{
			name: "multiple users",
			users: []*gitlab.User{
				{
					ID: 3,
				},
				{
					ID: 6,
				},
				{
					ID: 2,
				},
				{
					ID: 51,
				},
				{
					ID: 32,
				},
				{
					ID: 87,
				},
				{
					ID: 210,
				},
				{
					ID: 6493,
				},
				{
					ID: 50132,
				},
			},
			IDs: []int{
				50132,
				6493,
				210,
				87,
				32,
				51,
				2,
				3,
				6,
			},
		},
	}
	for _, tC := range testCases {
		t.Run(tC.name, func(t *testing.T) {
			got := IDsFromUsers(tC.users)
			assert.ElementsMatch(t, *got, tC.IDs)
		})
	}
}

func Test_LabelsPromptAPIFail(t *testing.T) {
	// mock glrepo.Remote object
	repo := glrepo.New("foo", "bar")
	remote := &git.Remote{
		Name:     "test",
		Resolved: "base",
	}
	repoRemote := &glrepo.Remote{
		Remote: remote,
		Repo:   repo,
	}

	api.ListLabels = func(_ *gitlab.Client, _ interface{}, _ *gitlab.ListLabelsOptions) ([]*gitlab.Label, error) {
		return nil, errors.New("API call failed")
	}

	var got []string
	err := LabelsPrompt(&got, &gitlab.Client{}, repoRemote)
	assert.Nil(t, got)
	assert.EqualError(t, err, "API call failed")
}

func Test_LabelsPromptPromptsFail(t *testing.T) {
	// mock glrepo.Remote object
	repo := glrepo.New("foo", "bar")
	remote := &git.Remote{
		Name:     "test",
		Resolved: "base",
	}
	repoRemote := &glrepo.Remote{
		Remote: remote,
		Repo:   repo,
	}

	t.Run("MultiSelect", func(t *testing.T) {
		// Return a list with at least one value so we hit the MultiSelect path
		api.ListLabels = func(_ *gitlab.Client, _ interface{}, _ *gitlab.ListLabelsOptions) ([]*gitlab.Label, error) {
			return []*gitlab.Label{
				{
					Name: "foo",
				},
			}, nil
		}

		as, restoreAsk := prompt.InitAskStubber()
		defer restoreAsk()

		as.Stub([]*prompt.QuestionStub{
			{
				Name:  "labels",
				Value: errors.New("MultiSelect prompt failed"),
			},
		})

		var got []string
		err := LabelsPrompt(&got, &gitlab.Client{}, repoRemote)
		assert.Nil(t, got)
		assert.EqualError(t, err, "MultiSelect prompt failed")
	})

	t.Run("AskQuestionWithInput", func(t *testing.T) {
		// Return an empty list so we hit the AskQuestionWithInput prompt path
		api.ListLabels = func(_ *gitlab.Client, _ interface{}, _ *gitlab.ListLabelsOptions) ([]*gitlab.Label, error) {
			return []*gitlab.Label{}, nil
		}

		as, restoreAsk := prompt.InitAskStubber()
		defer restoreAsk()

		as.Stub([]*prompt.QuestionStub{
			{
				Name:  "labels",
				Value: errors.New("AskQuestionWithInput prompt failed"),
			},
		})

		var got []string
		err := LabelsPrompt(&got, &gitlab.Client{}, repoRemote)
		assert.Nil(t, got)
		assert.EqualError(t, err, "AskQuestionWithInput prompt failed")
	})
}

func Test_LabelsPromptMultiSelect(t *testing.T) {
	// mock glrepo.Remote object
	repo := glrepo.New("foo", "bar")
	remote := &git.Remote{
		Name:     "test",
		Resolved: "base",
	}
	repoRemote := &glrepo.Remote{
		Remote: remote,
		Repo:   repo,
	}

	api.ListLabels = func(_ *gitlab.Client, _ interface{}, _ *gitlab.ListLabelsOptions) ([]*gitlab.Label, error) {
		return []*gitlab.Label{
			{
				Name: "foo",
			},
			{
				Name: "bar",
			},
			{
				Name: "baz",
			},
			{
				Name: "qux",
			},
			{
				Name: "quux",
			},
			{
				Name: "quz",
			},
		}, nil
	}

	testCases := []struct {
		name     string
		input    []string
		labels   []string // Can be set to have initial labels
		expected []string // expected labels
	}{
		{
			name:     "simple",
			input:    []string{"foo", "bar"},
			expected: []string{"foo", "bar"},
		},
		{
			name:     "respect-defined-labels",
			input:    []string{"foo"},
			labels:   []string{"bar"},
			expected: []string{"foo", "bar"},
		},
		{
			name: "nothing",
		},
		{
			name:     "nothing-but-respect-already-defined",
			labels:   []string{"qux"},
			expected: []string{"qux"},
		},
	}
	for _, tC := range testCases {
		t.Run(tC.name, func(t *testing.T) {
			as, restoreAsk := prompt.InitAskStubber()
			defer restoreAsk()

			as.Stub([]*prompt.QuestionStub{
				{
					Name:  "labels",
					Value: tC.input,
				},
			})

			err := LabelsPrompt(&tC.labels, &gitlab.Client{}, repoRemote)
			assert.NoError(t, err)
			assert.ElementsMatch(t, tC.labels, tC.expected)
		})
	}
}

func Test_LabelsPromptAskQuestionWithInput(t *testing.T) {
	// mock glrepo.Remote object
	repo := glrepo.New("foo", "bar")
	remote := &git.Remote{
		Name:     "test",
		Resolved: "base",
	}
	repoRemote := &glrepo.Remote{
		Remote: remote,
		Repo:   repo,
	}

	api.ListLabels = func(_ *gitlab.Client, _ interface{}, _ *gitlab.ListLabelsOptions) ([]*gitlab.Label, error) {
		return []*gitlab.Label{}, nil
	}

	testCases := []struct {
		name     string
		input    string
		labels   []string // Can be set to have initial labels
		expected []string // expected labels
	}{
		{
			name:     "simple",
			input:    "foo,bar",
			expected: []string{"foo", "bar"},
		},
		{
			name:     "respect-defined-labels",
			input:    "foo",
			labels:   []string{"bar"},
			expected: []string{"foo", "bar"},
		},
		{
			name: "nothing",
		},
		{
			name:     "nothing-but-respect-already-defined",
			labels:   []string{"qux"},
			expected: []string{"qux"},
		},
	}
	for _, tC := range testCases {
		t.Run(tC.name, func(t *testing.T) {
			as, restoreAsk := prompt.InitAskStubber()
			defer restoreAsk()

			as.Stub([]*prompt.QuestionStub{
				{
					Name:  "labels",
					Value: tC.input,
				},
			})

			err := LabelsPrompt(&tC.labels, &gitlab.Client{}, repoRemote)
			assert.NoError(t, err)
			assert.ElementsMatch(t, tC.labels, tC.expected)
		})
	}
}

func Test_ConfirmSubmission(t *testing.T) {
	const (
		submitLabel      = "Submit"
		previewLabel     = "Continue in browser"
		addMetadataLabel = "Add metadata"
		cancelLabel      = "Cancel"
	)

	t.Run("success", func(t *testing.T) {
		testCases := []struct {
			name             string
			input            string
			allowPreview     bool
			allowAddMetadata bool
			output           Action
		}{
			{
				name:   "submit",
				input:  submitLabel,
				output: SubmitAction,
			},
			{
				name:         "preview",
				input:        previewLabel,
				allowPreview: true,
				output:       PreviewAction,
			},
			{
				name:             "Add Metadata",
				input:            addMetadataLabel,
				allowAddMetadata: true,
				output:           AddMetadataAction,
			},
			{
				name:   "cancel",
				input:  cancelLabel,
				output: CancelAction,
			},
		}
		for _, tC := range testCases {
			t.Run(tC.name, func(t *testing.T) {
				as, restoreAsk := prompt.InitAskStubber()
				defer restoreAsk()

				as.Stub([]*prompt.QuestionStub{
					{
						Name:  "confirmation",
						Value: tC.input,
					},
				})

				var got Action
				got, err := ConfirmSubmission(tC.allowPreview, tC.allowAddMetadata)
				assert.NoError(t, err)
				assert.Equal(t, tC.output, got)
			})
		}
	})
	t.Run("failed", func(t *testing.T) {
		testCases := []struct {
			name   string
			input  interface{}
			output string
		}{
			{
				name:   "prompt",
				input:  errors.New("no terminal"),
				output: "could not prompt: no terminal",
			},
			{
				name:   "invalid value",
				input:  "does not exist",
				output: "invalid value: does not exist",
			},
		}
		for _, tC := range testCases {
			t.Run(tC.name, func(t *testing.T) {
				as, restoreAsk := prompt.InitAskStubber()
				defer restoreAsk()

				as.Stub([]*prompt.QuestionStub{
					{
						Name:  "confirmation",
						Value: tC.input,
					},
				})

				var got Action
				got, err := ConfirmSubmission(false, false)
				assert.Equal(t, Action(-1), got)
				assert.EqualError(t, err, tC.output)
			})
		}
	})
}

func TestListGitLabTemplates(t *testing.T) {
	tests := []struct {
		name          string
		give          string
		wantTemplates []string
		wantErr       bool
	}{
		{
			name:          "Get all the issues templates",
			give:          "issue_templates",
			wantTemplates: []string{"Bug", "Feature Request"},
		},
		{
			name:          "Get all the issues templates",
			give:          "merge_request_templates",
			wantTemplates: []string{"Default"},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			git.ToplevelDir = func() (string, error) { return "../../test/testdata", nil }
			gotTemplates, gotErr := ListGitLabTemplates(test.give)
			require.Equal(t, test.wantErr, (gotErr != nil))
			assert.EqualValues(t, test.wantTemplates, gotTemplates, "Templates got didn't match")
		})
	}
}
